diff --git a/api/lastfm.ts b/api/lastfm.ts index f382467..ffe93d9 100644 --- a/api/lastfm.ts +++ b/api/lastfm.ts @@ -62,6 +62,7 @@ export interface RecentTracksPage { export interface ScrobblePayload { artist: string; + albumArtist?: string; album?: string; title: string; timestamp: number; @@ -189,6 +190,10 @@ export async function scrobble( const suffix = inputs.length > 1 ? `[${i}]` : ""; body[`artist${suffix}`] = s.artist; + // according to last.fm's documentation, the album artist should *only* be sent if it differs from the artist. + if (s.albumArtist && s.albumArtist !== s.artist) { + body[`albumArtist${suffix}`] = s.albumArtist; + } body[`track${suffix}`] = s.title; body[`timestamp${suffix}`] = s.timestamp; if (s.album) { diff --git a/commands/import.ts b/commands/import.ts index e7760b1..b21e8c0 100644 --- a/commands/import.ts +++ b/commands/import.ts @@ -16,9 +16,9 @@ Arguments: Path to either a .scrobbler.log or .csv file. Options: - -n, --dry-run Simulate the import without scrobbling or modifying the file - -s, --no-skip-marker Don't mark any track as skipped after importing (doesn't touch the provided file in any way) - -h, --help Show this help message + -n, --dry-run Simulate the import without scrobbling or modifying the file + -s, --no-skip-marker Don't mark any track as skipped after importing (doesn't touch the provided file in any way) + -h, --help Show this help message `; export async function executeImportCommand(args: string[] = Deno.args): Promise> { diff --git a/lib/errors.ts b/lib/errors.ts index 08c60ff..4a047c9 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -53,11 +53,12 @@ export interface ConfigError extends TaggedError<"config", ConfigErrorReason> { export type CsvErrorReason = | IOError - | "invalid_columns"; + | "invalid_columns" + | "required_header_fields_missing"; export interface CsvError extends TaggedError<"csv", CsvErrorReason> { readonly message: string; - readonly path: string; + readonly path: string | undefined; } export interface AuthError extends BaseError<"auth"> { @@ -79,7 +80,7 @@ export const Errors = { config: (tag: ConfigErrorReason, message: string): ConfigError => ({ kind: "config", tag, message }), - csv: (tag: CsvErrorReason, message: string, path: string): CsvError => ({ kind: "csv", tag, message, path }), + csv: (tag: CsvErrorReason, message: string, path?: string | undefined): CsvError => ({ kind: "csv", tag, message, path }), auth: (message: string): AuthError => ({ kind: "auth", message }), diff --git a/lib/format/csv/codec.ts b/lib/format/csv/codec.ts index 3cb3dc2..4d57755 100644 --- a/lib/format/csv/codec.ts +++ b/lib/format/csv/codec.ts @@ -54,6 +54,7 @@ export function parseTrack( line: string, lineIndex: number, path: string, + header?: Record, ): Result { if (line === serializeHeader()) { return Fail(Errors.csv("invalid_columns", "header row", path)); @@ -71,11 +72,21 @@ export function parseTrack( ); } + if (!header) { + return Ok({ + artist: unescape(fields[0] ?? ""), + album: unescape(fields[1] ?? ""), + title: unescape(fields[2] ?? ""), + date: unescape(fields[3] ?? ""), + }); + } + return Ok({ - artist: unescape(fields[0] ?? ""), - album: unescape(fields[1] ?? ""), - title: unescape(fields[2] ?? ""), - date: unescape(fields[3] ?? ""), + artist: unescape(fields[header["artist"]]), + albumArtist: unescape(fields[header["album_artist"]] ?? ""), + album: unescape(fields[header["album"]] ?? ""), + title: unescape(fields[header["title"]]), + date: unescape(fields[header["date"]] ?? ""), }); } @@ -86,3 +97,31 @@ export function serializeTrack(track: DocumentTrack): string { export function serializeHeader(): string { return CSV_HEADER.join(","); } + +export function getColumnIndices(line: string): Result, ParseError> { + const fields = split(line); + const returns: Record = {}; + const requiredFields = ["artist", "title"]; + + CSV_HEADER.forEach((currentValue: string) => { + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + + if (currentValue === field) { + returns[currentValue] = i; + break; + } + } + }); + + for (const required of requiredFields) { + if (!Object.keys(returns).includes(required)) { + return Fail(Errors.csv( + "required_header_fields_missing", + `expected at least ${requiredFields.length} header fields (${requiredFields.join(", ")})`, + )); + } + } + + return Ok(returns); +} diff --git a/lib/format/csv/io.ts b/lib/format/csv/io.ts index 36ad68c..ef3cf2b 100644 --- a/lib/format/csv/io.ts +++ b/lib/format/csv/io.ts @@ -1,6 +1,6 @@ import { Errors } from "~/lib/errors.ts"; import { CsvDocument, DocumentTrack, SKIP_PREFIX } from "~/lib/format/csv/mod.ts"; -import { parseTrack, serializeHeader, serializeTrack } from "~/lib/format/csv/codec.ts"; +import { getColumnIndices, parseTrack, serializeHeader, serializeTrack } from "~/lib/format/csv/codec.ts"; import { Fail, Ok, Result } from "~/lib/result.ts"; type IOError = ReturnType; @@ -18,10 +18,15 @@ export async function loadCsvDocument(path: string): Promise