wip on autotagging

This commit is contained in:
2026-05-01 11:52:50 -04:00
parent 5cf019a0ac
commit d5140395da
295 changed files with 11105 additions and 40714 deletions
+2
View File
@@ -19,6 +19,7 @@ import '@components/track-details/track-details.ts';
import '@components/explore-view/explore-view.ts';
import '@components/explore-artist-details/explore-artist-details.js';
import '@components/explore-album-details/explore-album-details.js';
import '@components/autotag-view/autotag-view.ts';
import '@awesome.me/webawesome/dist/styles/themes/default.css';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js';
@@ -59,6 +60,7 @@ const VIEW_TAGS: Record<string, string> = {
genres: 'genres-view',
playlists: 'playlist-view',
explore: 'explore-view',
autotag: 'autotag-view',
settings: 'config-page',
};
File diff suppressed because it is too large Load Diff
@@ -1939,10 +1939,6 @@ export class ConfigPage extends LitElement {
// --- Search / Index section ---
private async pollIndexStatus(): Promise<void> {
// Kept as no-op — status comes via events now.
}
private renderSearchSection() {
const s = this.indexStatus;
@@ -492,6 +492,10 @@ export class CoverGrid
override connectedCallback() {
super.connectedCallback();
// Reference renderSplitGrid so the deferred split-grid
// render path (and its track-event helpers) doesn't trip
// noUnusedLocals. Never invoked at runtime.
void this.renderSplitGrid;
this.restoreSortPreferences();
this.loadAlbums();
@@ -998,27 +1002,6 @@ export class CoverGrid
}
}
/**
* Synchronise the dropdown to the current
* selection: open the sole selected album's
* dropdown, or close it when zero or many
* albums are selected.
*/
private syncDropdownToSelection() {
if (this.selectedAlbums.size === 1) {
const [albumId] = this.selectedAlbums;
const album = this.cachedFilteredAlbums.find(
(a) => a.ID === albumId,
);
if (album) {
void this.openDropdown(album);
}
} else {
this.closeDropdown();
}
}
/* ====================================================================
* Event delegation helpers
* ==================================================================== */
@@ -1923,7 +1906,9 @@ export class CoverGrid
/**
* Dual virtualizer — dropdown sandwiched between
* "before" and "after" grids.
* "before" and "after" grids. Currently unreferenced
* (the single-grid path is the active rendering mode);
* kept here against the deferred split-grid layout.
*/
private renderSplitGrid() {
const sm = this.scrollMgr;
@@ -1978,6 +1963,7 @@ export class CoverGrid
`;
}
/** Context menu + playlist submenu popups. */
/** Trigger playlist submenu with resolved file paths. */
private async handleShowPlaylistSubmenu() {
@@ -6,36 +6,20 @@ import {
BrowseReleases,
GetThumbnail,
} from '@go/explore/Service';
import type {
MBReleaseGroup,
MBRelease,
MBTrack,
} from '@go/explore/Service';
import { GetAlbumTracks } from '@go/library/Library';
import { library } from '@go/models';
import type { explore } from '@go/models';
type MBReleaseGroup = explore.MBReleaseGroup;
type MBRelease = explore.MBRelease;
type MBTrack = explore.MBTrack;
import { exploreCache } from '../../store/explore-cache';
import { exploreSettings } from '../../store/explore-settings';
import { libraryStore } from '../../store/library-store';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '../library-status-indicator/library-status-indicator.js';
/* ── Constants ── */
const CAA_GROUP_BASE = 'https://coverartarchive.org/release-group';
/* ── Utility functions (duplicated per Knowledge Pattern #9 — no cross-component imports) ── */
function CoverArtGroupURL(releaseGroupMBID: string): string {
return `${CAA_GROUP_BASE}/${releaseGroupMBID}/front-250`;
}
function nameToHue(name: string): number {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return Math.abs(hash) % 360;
}
function extractYear(dateStr: string): string {
if (!dateStr) return '';
return dateStr.substring(0, 4);
@@ -598,7 +582,9 @@ export class ExploreAlbumDetails extends LitElement {
}
// Phase 1: hydrate tracklist from local library if available.
const localRelease = await this.hydrateFromLibrary(mbid);
// Awaited for the side effect of populating the local
// tracklist; the return value isn't currently consumed.
await this.hydrateFromLibrary(mbid);
// Library-only mode: local data is all we show.
if (exploreSettings.libraryOnly) {
@@ -899,7 +885,7 @@ export class ExploreAlbumDetails extends LitElement {
});
const cluster: ReleaseCluster = {
representative: sorted[0],
representative: sorted[0]!,
allReleases: sorted,
fingerprint,
score: 0, // filled in below
@@ -16,13 +16,12 @@ import {
GetTrackThumbnails,
ResolveReleaseGroupMBIDs,
} from '@go/explore/Service';
import type {
MBArtist,
MBReleaseGroup,
LBTopRecording,
LBTopReleaseGroup,
LBSimilarArtist,
} from '@go/explore/Service';
import type { explore } from '@go/models';
type MBArtist = explore.MBArtist;
type MBReleaseGroup = explore.MBReleaseGroup;
type LBTopRecording = explore.LBTopRecording;
type LBTopReleaseGroup = explore.LBTopReleaseGroup;
type LBSimilarArtist = explore.LBSimilarArtist;
import { exploreCache } from '../../store/explore-cache';
import { exploreSettings } from '../../store/explore-settings';
import { libraryStore } from '../../store/library-store';
@@ -31,7 +30,6 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '../library-status-indicator/library-status-indicator.js';
/* ── Constants ── */
const CAA_GROUP_BASE = 'https://coverartarchive.org/release-group';
/** Desired section order for grouping release types. */
const TYPE_ORDER = ['Albums', 'EP', 'Single', 'Other Albums'];
@@ -55,14 +53,6 @@ const NON_STUDIO_SECONDARY_TYPES = new Set([
/* ── Utility functions (duplicated from explore-view per design decision) ── */
function CoverArtGroupURL(releaseGroupMBID: string): string {
return `${CAA_GROUP_BASE}/${releaseGroupMBID}/front-250`;
}
function CoverArtReleaseURL(releaseMBID: string): string {
return `https://coverartarchive.org/release/${releaseMBID}/front-250`;
}
function nameToHue(name: string): number {
let hash = 0;
for (let i = 0; i < name.length; i++) {
@@ -108,7 +98,6 @@ export class ExploreArtistDetails extends LitElement {
@state() private loadingTopReleases = true;
@state() private loadingReleases = true;
@state() private errorArtist = '';
@state() private errorTracks = '';
@state() private errorReleases = '';
@state() private similarArtists: LBSimilarArtist[] = [];
@state() private loadingSimilar = true;
@@ -1297,7 +1286,6 @@ export class ExploreArtistDetails extends LitElement {
void this.batchResolveTrackThumbnails(tracks, mapping);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.errorTracks = msg;
console.error(
`[explore-artist] TopRecordingsForArtist error: ${msg}`,
);
@@ -1436,26 +1424,6 @@ export class ExploreArtistDetails extends LitElement {
}
}
/**
* Resolve a cover art thumbnail for a release group (or release)
* via the backend proxy (local library → disk cache → CAA).
* Updates thumbnailURLs reactively so the image pops in when ready.
*/
private resolveThumbnail(mbid: string, albumName: string, artistName: string) {
if (!mbid || this.thumbnailURLs.has(mbid)) return;
// Mark as in-flight so we don't fire duplicate requests.
this.thumbnailURLs.set(mbid, '');
GetThumbnail(mbid, albumName, artistName)
.then((url) => {
if (url) {
this.thumbnailURLs = new Map(this.thumbnailURLs).set(mbid, url);
}
})
.catch(() => {});
}
/**
* Batch-resolve thumbnails in two phases:
* 1. Batch call for cached/local art — instant.
@@ -2,13 +2,6 @@ import { LitElement, html, css, nothing } from 'lit';
import { customElement, state, query as litQuery } from 'lit/decorators.js';
import { designTokens } from '../../styles/tokens.css';
import { Search, GetThumbnail, GetThumbnails, GetArtistImageURL, GetPopularityBatch, RecordSearchClick } from '@go/explore/Service';
import type { ThumbnailRequest } from '@go/explore/Service';
import type {
MBSearchResult,
MBArtist,
MBReleaseGroup,
MBRecording,
} from '@go/explore/Service';
import { libraryStore } from '../../store/library-store';
import { exploreCache } from '../../store/explore-cache';
import { exploreSettings } from '../../store/explore-settings';
@@ -16,6 +9,11 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '../library-status-indicator/library-status-indicator.js';
import '../top-results-row/top-results-row.js';
import type { explore } from '@go/models';
type ThumbnailRequest = explore.ThumbnailRequest;
type MBSearchResult = explore.MBSearchResult;
type MBArtist = explore.MBArtist;
type MBReleaseGroup = explore.MBReleaseGroup;
type MBRecording = explore.MBRecording;
/* ── Constants ── */
const DEBOUNCE_MS = 300;
@@ -32,20 +30,20 @@ function editDistance(a: string, b: string): number {
const matrix: number[][] = [];
for (let i = 0; i <= a.length; i++) matrix[i] = [i];
for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
for (let j = 0; j <= b.length; j++) matrix[0]![j] = j;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost,
matrix[i]![j] = Math.min(
matrix[i - 1]![j]! + 1,
matrix[i]![j - 1]! + 1,
matrix[i - 1]![j - 1]! + cost,
);
}
}
return matrix[a.length][b.length];
return matrix[a.length]![b.length]!;
}
/**
@@ -72,16 +70,7 @@ function fuzzyMatch(query: string, name: string): boolean {
);
}
const MAX_SECTION_RESULTS = 10;
const CAA_GROUP_BASE = 'https://coverartarchive.org/release-group';
/**
* Build a Cover Art Archive URL for a release-group front cover.
* Mirrors the Wails CoverArtGroupURL binding but runs synchronously
* on the frontend — avoids N async round-trips per render cycle.
*/
function CoverArtGroupURL(releaseGroupMBID: string): string {
return `${CAA_GROUP_BASE}/${releaseGroupMBID}/front-250`;
}
/** Hash a string to a hue value 0360 for avatar coloring. */
function nameToHue(name: string): number {
@@ -894,9 +883,9 @@ export class ExploreView extends LitElement {
// Collect all non-empty MBIDs.
const mbids: string[] = [];
for (const a of result.artists) if (a.mbid) mbids.push(a.mbid);
for (const rg of result.releaseGroups) if (rg.mbid) mbids.push(rg.mbid);
for (const r of result.recordings) if (r.mbid) mbids.push(r.mbid);
for (const a of result.artists ?? []) if (a.mbid) mbids.push(a.mbid);
for (const rg of result.releaseGroups ?? []) if (rg.mbid) mbids.push(rg.mbid);
for (const r of result.recordings ?? []) if (r.mbid) mbids.push(r.mbid);
if (mbids.length === 0) return;
@@ -927,16 +916,22 @@ export class ExploreView extends LitElement {
return 0.35 * relevance + 0.50 * logPop + 0.15 * personal;
};
// Re-sort each category.
result.artists.sort((a, b) =>
blendedScore(b.mbid, b.score) - blendedScore(a.mbid, a.score));
result.releaseGroups.sort((a, b) =>
blendedScore(b.mbid, b.score) - blendedScore(a.mbid, a.score));
result.recordings.sort((a, b) =>
blendedScore(b.mbid, b.score) - blendedScore(a.mbid, a.score));
// Re-sort each category. Backend stamps a `score` field
// onto entries before returning them, but the Wails-
// generated MB types don't model it — cast through any to
// read it on the way to the comparator.
const cmp = (a: { mbid: string }, b: { mbid: string }): number =>
blendedScore(b.mbid, (b as any).score ?? 0) - blendedScore(a.mbid, (a as any).score ?? 0);
(result.artists ?? []).sort(cmp);
(result.releaseGroups ?? []).sort(cmp);
(result.recordings ?? []).sort(cmp);
// Trigger re-render.
this.results = { ...result };
// Trigger re-render. MBSearchResult is a Wails-generated
// class with bound methods (convertValues), so request an
// update directly rather than spreading the object — that
// would drop the methods.
this.results = result;
this.requestUpdate();
}
/**
@@ -967,8 +962,7 @@ export class ExploreView extends LitElement {
// Enrich artists: if MB result matches a library artist, add local images.
if (result.artists) {
for (let i = 0; i < result.artists.length; i++) {
const a = result.artists[i];
for (const a of result.artists) {
const lib = (a.mbid && libArtistsByMBID.get(a.mbid)) ||
libArtistsByName.get(a.name.toLowerCase());
if (lib) {
@@ -981,8 +975,7 @@ export class ExploreView extends LitElement {
// Enrich release groups: if MB result matches a library album, use local art.
if (result.releaseGroups) {
for (let i = 0; i < result.releaseGroups.length; i++) {
const rg = result.releaseGroups[i];
for (const rg of result.releaseGroups) {
const lib = rg.mbid ? libAlbumsByMBID.get(rg.mbid) : undefined;
if (lib) {
(rg as any)._coverArt = lib.CoverArtMedium || lib.CoverArtSmall || '';
@@ -1020,7 +1013,7 @@ export class ExploreView extends LitElement {
if (prev.releaseGroups?.length) {
const existing = new Set(
(full.releaseGroups || []).map(
(rg) => `${rg.title}|${rg.artistCredit}`.toLowerCase(),
(rg: MBReleaseGroup) => `${rg.title}|${rg.artistCredit}`.toLowerCase(),
),
);
for (const rg of prev.releaseGroups) {
@@ -1,6 +1,7 @@
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { LibraryController } from '@store/controllers/library-controller';
import { libraryStore } from '@store/library-store';
import type { library } from '@go/models';
import { designTokens } from '../../styles/tokens.css';
@@ -48,9 +49,25 @@ export class LibraryFilter extends LitElement {
}
`];
private unsubscribeStore?: () => void;
override connectedCallback() {
super.connectedCallback();
this.loadLibraries();
// The LibraryController already wires a reactive subscription to
// libraryStore so the host re-renders on change, but it doesn't
// refresh our cached `this.libraries` array. Subscribe directly
// and re-fetch so LibraryAdded / Removed / Renamed events flow
// into the dropdown without a restart.
this.unsubscribeStore = libraryStore.subscribe(() => {
this.loadLibraries();
});
}
override disconnectedCallback() {
super.disconnectedCallback();
this.unsubscribeStore?.();
}
private async loadLibraries() {
@@ -203,7 +203,7 @@ export class LibraryStatusIndicator extends LitElement {
}
function capitalize(s: string): string {
return s.length > 0 ? s[0].toUpperCase() + s.slice(1) : s;
return s.length > 0 ? (s[0] ?? '').toUpperCase() + s.slice(1) : s;
}
declare global {
@@ -5,7 +5,7 @@ import { designTokens } from '../../styles/tokens.css';
import type { DragActiveDetail } from '@utils/drag-controller';
type View = 'home' | 'playlists' | 'artists' | 'genres' | 'albums' | 'tracks' | 'explore' | 'settings';
type View = 'home' | 'playlists' | 'artists' | 'genres' | 'albums' | 'tracks' | 'explore' | 'autotag' | 'settings';
interface NavItem {
id: View;
@@ -149,6 +149,7 @@ export class AppSidebar extends LitElement {
{ id: 'albums', label: 'Albums', icon: 'compact-disc' },
{ id: 'tracks', label: 'Tracks', icon: 'music' },
{ id: 'explore', label: 'Explore', icon: 'globe' },
{ id: 'autotag', label: 'Autotag', icon: 'tag' },
{ id: 'settings', label: 'Settings', icon: 'gear' },
];
@@ -49,7 +49,7 @@ export class TopResultsRow extends LitElement {
// Per-card state: cover images.
private images = new Map<string, string>();
static styles = [
static override styles = [
designTokens,
css`
:host {
@@ -177,7 +177,7 @@ export class TopResultsRow extends LitElement {
`,
];
updated(changed: Map<string, unknown>) {
override updated(changed: Map<string, unknown>) {
if (changed.has('results')) {
this.loadCardData();
}
@@ -227,7 +227,7 @@ export class TopResultsRow extends LitElement {
);
}
render() {
override render() {
if (!this.results?.length) return nothing;
return html`
@@ -19,7 +19,7 @@ import {
CancelBatchWrite,
} from '@go/tagwriter/TagWriter';
import { GetTrackMBIDs } from '@go/library/Library';
import type { TrackMBIDs } from '@go/library/Library';
type TrackMBIDs = library.TrackMBIDs;
import { ImageFilePicker, ReadFile } from '@go/frontendutil/FrontendUtil';
import { libraryStore } from '../../store/library-store';
import { EventsOn, EventsOff } from '@runtime/runtime';
@@ -1646,7 +1646,9 @@ export class TrackDetails extends LitElement {
// shows updated values and cover art. The store
// invalidation is already in-flight from the event;
// these calls await the pending fetch or start one.
const [tracks, albums] = await Promise.all([
// Awaited for the side effect of refreshing the
// store; the album list is consumed elsewhere.
const [tracks] = await Promise.all([
libraryStore.getTracks(),
libraryStore.getAlbums(),
]);
@@ -1776,8 +1778,9 @@ export class TrackDetails extends LitElement {
this.errorMessage = '';
this.cleanupPendingCoverArt();
// Refresh data from library store.
const [tracks, albums] = await Promise.all([
// Refresh data from library store; album list is
// refreshed for side effects only.
const [tracks] = await Promise.all([
libraryStore.getTracks(),
libraryStore.getAlbums(),
]);
+9
View File
@@ -52,6 +52,15 @@ export const Events = {
TrackMetadataChanged: "TrackMetadataChanged",
BatchWriteProgress: "BatchWriteProgress",
// Autotag apply events — emitted while an async ApplyAsync job is in flight so the review UI can render per-folder progress
AutotagApplyStarted: "AutotagApplyStarted",
AutotagApplyProgress: "AutotagApplyProgress",
AutotagApplyFinished: "AutotagApplyFinished",
// Autotag prefetch events — emitted by the background worker that scores pending tagging items so sidebar pills populate without the user having to open each folder
AutotagPrefetchProgress: "AutotagPrefetchProgress",
AutotagPrefetchFinished: "AutotagPrefetchFinished",
// Explore / search index events
IndexStatusChanged: "IndexStatusChanged",
} as const;
+3 -1
View File
@@ -10,7 +10,9 @@
* album detail page → check cache before API calls
*/
import type { MBReleaseGroup, LBTopRecording } from '@go/explore/Service';
import type { explore } from '@go/models';
type MBReleaseGroup = explore.MBReleaseGroup;
type LBTopRecording = explore.LBTopRecording;
/** Cached artist data from search results. */
export interface CachedArtist {
+48
View File
@@ -0,0 +1,48 @@
// Shared helpers for grouping a tracklist by disc number — used by
// both the explore album view and the autotag review UI so the
// rendering rules stay consistent (single-disc albums skip the
// "Disc 1" separator, multi-disc albums show one per disc).
export interface Disced {
discNumber?: number;
position?: number;
}
/**
* Returns true when any track has a discNumber > 1. A list with
* only disc 1 (or no discNumber set) renders without "Disc N"
* headers.
*/
export function isMultiDisc<T extends Disced>(tracks: T[]): boolean {
return tracks.some((t) => (t.discNumber ?? 1) > 1);
}
/**
* Group tracks by disc number, sorting tracks within each disc
* by position. Discs with no number default to disc 1, which
* matches MusicBrainz behaviour for releases that omit the field.
*/
export function groupByDisc<T extends Disced>(tracks: T[]): Map<number, T[]> {
const discMap = new Map<number, T[]>();
for (const track of tracks) {
const disc = track.discNumber ?? 1;
const bucket = discMap.get(disc);
if (bucket) {
bucket.push(track);
} else {
discMap.set(disc, [track]);
}
}
for (const bucket of discMap.values()) {
bucket.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
}
return discMap;
}
/** Returns disc numbers in ascending order from a grouped map. */
export function discNumbers<T>(discMap: Map<number, T[]>): number[] {
return [...discMap.keys()].sort((a, b) => a - b);
}
+92
View File
@@ -0,0 +1,92 @@
// Inline text diff for the autotag review UI. Splits both sides
// into tokens (word runs, whitespace runs, individual punctuation
// chars), runs LCS, and emits a flat segment list the renderer
// drops into spans. Punctuation is its own token so an apostrophe
// type swap (' vs ') shows just the apostrophe as changed instead
// of the whole word — that's the case the visible-but-identical
// titles in the autotag view were tripping on.
export type SegmentType = 'equal' | 'remove' | 'add';
export interface DiffSegment {
type: SegmentType;
text: string;
}
const tokenRe = /(\w+|\s+|[^\w\s])/g;
function tokenize(s: string): string[] {
return s.match(tokenRe) ?? [];
}
/**
* Compute an inline word/punct-level diff between `a` (old) and
* `b` (new), returning a list of segments suitable for inline
* rendering: equal segments come from both sides, remove segments
* come from `a` only, add segments come from `b` only. Adjacent
* segments of the same type are coalesced. Both sides empty
* returns a single empty equal segment.
*/
export function inlineDiff(a: string, b: string): DiffSegment[] {
if (a === b) {
return [{ type: 'equal', text: a }];
}
if (a === '') {
return [{ type: 'add', text: b }];
}
if (b === '') {
return [{ type: 'remove', text: a }];
}
const ta = tokenize(a);
const tb = tokenize(b);
const m = ta.length;
const n = tb.length;
// LCS table — O(m*n) memory. Track titles cap out at ~100
// tokens so this stays trivially small.
const dp: number[][] = Array.from({ length: m + 1 }, () =>
new Array<number>(n + 1).fill(0),
);
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (ta[i - 1] === tb[j - 1]) {
dp[i]![j] = dp[i - 1]![j - 1]! + 1;
} else {
dp[i]![j] = Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!);
}
}
}
// Backtrack to build the op list (reversed).
const ops: DiffSegment[] = [];
let i = m;
let j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && ta[i - 1] === tb[j - 1]) {
ops.push({ type: 'equal', text: ta[i - 1]! });
i--;
j--;
} else if (j > 0 && (i === 0 || dp[i]![j - 1]! >= dp[i - 1]![j]!)) {
ops.push({ type: 'add', text: tb[j - 1]! });
j--;
} else {
ops.push({ type: 'remove', text: ta[i - 1]! });
i--;
}
}
ops.reverse();
// Coalesce adjacent same-type segments so the renderer outputs
// one span per visual run instead of per token.
const merged: DiffSegment[] = [];
for (const seg of ops) {
const last = merged[merged.length - 1];
if (last && last.type === seg.type) {
last.text += seg.text;
} else {
merged.push({ type: seg.type, text: seg.text });
}
}
return merged;
}
+36
View File
@@ -0,0 +1,36 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {autotagservice} from '../models';
import {context} from '../models';
export function AckLibraryWarning(arg1:number):Promise<void>;
export function Apply(arg1:string,arg2:string):Promise<autotagservice.ApplyResultView>;
export function ApplyAsync(arg1:string,arg2:string):Promise<void>;
export function ClearCompletedEntries(arg1:number):Promise<void>;
export function GetCandidateCoverArt(arg1:string,arg2:string):Promise<string>;
export function GetCandidates(arg1:string):Promise<autotagservice.ScoreView>;
export function GetCandidatesForPasteURL(arg1:string,arg2:string):Promise<autotagservice.ScoreView>;
export function GetNextPending():Promise<autotagservice.PendingItem>;
export function GetPendingFolder(arg1:string):Promise<autotagservice.PendingItem>;
export function LeaveAsIs(arg1:string):Promise<void>;
export function ListPendingFolders(arg1:number):Promise<Array<autotagservice.PendingItem>>;
export function RetagGroup(arg1:string):Promise<void>;
export function SetContext(arg1:context.Context):Promise<void>;
export function Skip(arg1:string):Promise<void>;
export function StartAutotagQueue(arg1:number):Promise<void>;
export function StartBackgroundPrefetch():Promise<void>;
+67
View File
@@ -0,0 +1,67 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AckLibraryWarning(arg1) {
return window['go']['autotagservice']['Service']['AckLibraryWarning'](arg1);
}
export function Apply(arg1, arg2) {
return window['go']['autotagservice']['Service']['Apply'](arg1, arg2);
}
export function ApplyAsync(arg1, arg2) {
return window['go']['autotagservice']['Service']['ApplyAsync'](arg1, arg2);
}
export function ClearCompletedEntries(arg1) {
return window['go']['autotagservice']['Service']['ClearCompletedEntries'](arg1);
}
export function GetCandidateCoverArt(arg1, arg2) {
return window['go']['autotagservice']['Service']['GetCandidateCoverArt'](arg1, arg2);
}
export function GetCandidates(arg1) {
return window['go']['autotagservice']['Service']['GetCandidates'](arg1);
}
export function GetCandidatesForPasteURL(arg1, arg2) {
return window['go']['autotagservice']['Service']['GetCandidatesForPasteURL'](arg1, arg2);
}
export function GetNextPending() {
return window['go']['autotagservice']['Service']['GetNextPending']();
}
export function GetPendingFolder(arg1) {
return window['go']['autotagservice']['Service']['GetPendingFolder'](arg1);
}
export function LeaveAsIs(arg1) {
return window['go']['autotagservice']['Service']['LeaveAsIs'](arg1);
}
export function ListPendingFolders(arg1) {
return window['go']['autotagservice']['Service']['ListPendingFolders'](arg1);
}
export function RetagGroup(arg1) {
return window['go']['autotagservice']['Service']['RetagGroup'](arg1);
}
export function SetContext(arg1) {
return window['go']['autotagservice']['Service']['SetContext'](arg1);
}
export function Skip(arg1) {
return window['go']['autotagservice']['Service']['Skip'](arg1);
}
export function StartAutotagQueue(arg1) {
return window['go']['autotagservice']['Service']['StartAutotagQueue'](arg1);
}
export function StartBackgroundPrefetch() {
return window['go']['autotagservice']['Service']['StartBackgroundPrefetch']();
}
+6
View File
@@ -7,6 +7,8 @@ export function BrowseReleaseGroups(arg1:string):Promise<Array<explore.MBRelease
export function BrowseReleases(arg1:string):Promise<Array<explore.MBRelease>>;
export function CAALimiter():Promise<explore.RateLimiter>;
export function CheckLibraryMBIDs(arg1:Array<string>):Promise<Record<string, string>>;
export function CoverArtGroupURL(arg1:string):Promise<string>;
@@ -25,6 +27,8 @@ export function GetArtistMBID(arg1:string):Promise<string>;
export function GetArtistPlayCount(arg1:string):Promise<number>;
export function GetCandidateThumbnail(arg1:string,arg2:string):Promise<string>;
export function GetIndexStatus():Promise<explore.IndexStatus>;
export function GetLibrarySimilarArtists(arg1:string):Promise<Array<explore.LBSimilarArtist>>;
@@ -49,6 +53,8 @@ export function LookupArtist(arg1:string):Promise<explore.MBArtist>;
export function LookupReleaseGroup(arg1:string):Promise<explore.MBReleaseGroup>;
export function MusicBrainz():Promise<explore.MusicBrainzClient>;
export function PopulateLocalCrossReferences():Promise<void>;
export function RecordSearchClick(arg1:string,arg2:string,arg3:string):Promise<void>;
+12
View File
@@ -10,6 +10,10 @@ export function BrowseReleases(arg1) {
return window['go']['explore']['Service']['BrowseReleases'](arg1);
}
export function CAALimiter() {
return window['go']['explore']['Service']['CAALimiter']();
}
export function CheckLibraryMBIDs(arg1) {
return window['go']['explore']['Service']['CheckLibraryMBIDs'](arg1);
}
@@ -46,6 +50,10 @@ export function GetArtistPlayCount(arg1) {
return window['go']['explore']['Service']['GetArtistPlayCount'](arg1);
}
export function GetCandidateThumbnail(arg1, arg2) {
return window['go']['explore']['Service']['GetCandidateThumbnail'](arg1, arg2);
}
export function GetIndexStatus() {
return window['go']['explore']['Service']['GetIndexStatus']();
}
@@ -94,6 +102,10 @@ export function LookupReleaseGroup(arg1) {
return window['go']['explore']['Service']['LookupReleaseGroup'](arg1);
}
export function MusicBrainz() {
return window['go']['explore']['Service']['MusicBrainz']();
}
export function PopulateLocalCrossReferences() {
return window['go']['explore']['Service']['PopulateLocalCrossReferences']();
}
+289
View File
@@ -1,3 +1,262 @@
export namespace autotagservice {
export class AlignmentView {
localIndex: number;
localTitle: string;
localLengthMillis: number;
candidatePosition: number;
candidateDiscNumber: number;
candidateTitle: string;
candidateMbid: string;
candidateLength: number;
titleScore: number;
lengthDeltaMs: number;
trackNumberOk: boolean;
status: string;
static createFrom(source: any = {}) {
return new AlignmentView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.localIndex = source["localIndex"];
this.localTitle = source["localTitle"];
this.localLengthMillis = source["localLengthMillis"];
this.candidatePosition = source["candidatePosition"];
this.candidateDiscNumber = source["candidateDiscNumber"];
this.candidateTitle = source["candidateTitle"];
this.candidateMbid = source["candidateMbid"];
this.candidateLength = source["candidateLength"];
this.titleScore = source["titleScore"];
this.lengthDeltaMs = source["lengthDeltaMs"];
this.trackNumberOk = source["trackNumberOk"];
this.status = source["status"];
}
}
export class FailureView {
filePath: string;
error: string;
static createFrom(source: any = {}) {
return new FailureView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.filePath = source["filePath"];
this.error = source["error"];
}
}
export class ApplyResultView {
groupKey: string;
succeeded: number;
failed: number;
failures: FailureView[];
static createFrom(source: any = {}) {
return new ApplyResultView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.groupKey = source["groupKey"];
this.succeeded = source["succeeded"];
this.failed = source["failed"];
this.failures = this.convertValues(source["failures"], FailureView);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class ScoreBreakdownView {
titleAvg: number;
lengthAvg: number;
trackCountFit: number;
releaseMeta: number;
static createFrom(source: any = {}) {
return new ScoreBreakdownView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.titleAvg = source["titleAvg"];
this.lengthAvg = source["lengthAvg"];
this.trackCountFit = source["trackCountFit"];
this.releaseMeta = source["releaseMeta"];
}
}
export class CandidateView {
releaseMbid: string;
releaseGroupMbid: string;
title: string;
artistCredit: string;
date: string;
originalDate: string;
country: string;
status: string;
trackCount: number;
score: number;
breakdown: ScoreBreakdownView;
source: string;
provenance: string;
coverArtUrl: string;
alignments: AlignmentView[];
static createFrom(source: any = {}) {
return new CandidateView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.releaseMbid = source["releaseMbid"];
this.releaseGroupMbid = source["releaseGroupMbid"];
this.title = source["title"];
this.artistCredit = source["artistCredit"];
this.date = source["date"];
this.originalDate = source["originalDate"];
this.country = source["country"];
this.status = source["status"];
this.trackCount = source["trackCount"];
this.score = source["score"];
this.breakdown = this.convertValues(source["breakdown"], ScoreBreakdownView);
this.source = source["source"];
this.provenance = source["provenance"];
this.coverArtUrl = source["coverArtUrl"];
this.alignments = this.convertValues(source["alignments"], AlignmentView);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class LocalTrackView {
audioFileId: number;
filePath: string;
title: string;
artist: string;
trackNumber: number;
discNumber: number;
lengthMillis: number;
recordingMbid: string;
static createFrom(source: any = {}) {
return new LocalTrackView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.audioFileId = source["audioFileId"];
this.filePath = source["filePath"];
this.title = source["title"];
this.artist = source["artist"];
this.trackNumber = source["trackNumber"];
this.discNumber = source["discNumber"];
this.lengthMillis = source["lengthMillis"];
this.recordingMbid = source["recordingMbid"];
}
}
export class PendingItem {
groupKey: string;
libraryId: number;
libraryName: string;
folderSubPath: string;
trackCount: number;
albumName: string;
albumArtist: string;
discNumber: number;
bestMatchReleaseMbid: string;
score: number;
status: string;
static createFrom(source: any = {}) {
return new PendingItem(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.groupKey = source["groupKey"];
this.libraryId = source["libraryId"];
this.libraryName = source["libraryName"];
this.folderSubPath = source["folderSubPath"];
this.trackCount = source["trackCount"];
this.albumName = source["albumName"];
this.albumArtist = source["albumArtist"];
this.discNumber = source["discNumber"];
this.bestMatchReleaseMbid = source["bestMatchReleaseMbid"];
this.score = source["score"];
this.status = source["status"];
}
}
export class ScoreView {
groupKey: string;
localTracks: LocalTrackView[];
candidates: CandidateView[];
static createFrom(source: any = {}) {
return new ScoreView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.groupKey = source["groupKey"];
this.localTracks = this.convertValues(source["localTracks"], LocalTrackView);
this.candidates = this.convertValues(source["candidates"], CandidateView);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}
export namespace explore {
export class TierStatus {
@@ -228,6 +487,7 @@ export namespace explore {
date: string;
country: string;
status: string;
artistCredit?: string;
tracks?: MBTrack[];
static createFrom(source: any = {}) {
@@ -241,6 +501,7 @@ export namespace explore {
this.date = source["date"];
this.country = source["country"];
this.status = source["status"];
this.artistCredit = source["artistCredit"];
this.tracks = this.convertValues(source["tracks"], MBTrack);
}
@@ -361,6 +622,30 @@ export namespace explore {
}
}
export class MusicBrainzClient {
static createFrom(source: any = {}) {
return new MusicBrainzClient(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
}
}
export class RateLimiter {
static createFrom(source: any = {}) {
return new RateLimiter(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
}
}
export class ThumbnailRequest {
mbid: string;
albumName: string;
@@ -414,6 +699,7 @@ export namespace library {
CoverArtMedium: string;
CoverArtLarge: string;
Year: number;
ReleaseYear: number;
static createFrom(source: any = {}) {
return new Album(source);
@@ -430,6 +716,7 @@ export namespace library {
this.CoverArtMedium = source["CoverArtMedium"];
this.CoverArtLarge = source["CoverArtLarge"];
this.Year = source["Year"];
this.ReleaseYear = source["ReleaseYear"];
}
}
export class Artist {
@@ -1110,6 +1397,7 @@ export namespace sqlcgen {
Path: string;
// Go type: time
CreatedAt: any;
AutotagWarningAcked: number;
static createFrom(source: any = {}) {
return new Library(source);
@@ -1121,6 +1409,7 @@ export namespace sqlcgen {
this.Name = source["Name"];
this.Path = source["Path"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.AutotagWarningAcked = source["AutotagWarningAcked"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {