Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28428aa362 | ||
|
|
d777301c20 | ||
|
|
0a2736e893 | ||
|
|
8c3a23ad14 |
No files matched your search
+125
-1
@@ -602,6 +602,130 @@ app.post('/api/download', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/fix-artist-sort — batch-update sort tags + rename artist folder for all songs by a kanji artist
|
||||
app.post('/api/fix-artist-sort', async (req, res) => {
|
||||
const { ndUrl, user, pass, artistKanji, artistSort, jobId: clientJobId } = req.body;
|
||||
if (!ndUrl || !user || !pass || !artistKanji || !artistSort)
|
||||
return res.status(400).json({ error: 'ndUrl, user, pass, artistKanji, artistSort required' });
|
||||
|
||||
const jobId = clientJobId || ('job_' + Date.now());
|
||||
jobs.set(jobId, {
|
||||
id: jobId, type: 'fix-sort',
|
||||
artist: artistKanji, title: `Romaji-Fix: ${artistSort}`, album: '',
|
||||
status: 'queued', progress: 0, message: 'Suche Songs…',
|
||||
log: [], createdAt: Date.now(), updatedAt: Date.now(),
|
||||
});
|
||||
res.json({ jobId, status: 'queued' });
|
||||
|
||||
(async () => {
|
||||
const mkTok = () => {
|
||||
const s = Math.random().toString(36).slice(2, 10);
|
||||
return { s, t: crypto.createHash('md5').update(pass + s).digest('hex') };
|
||||
};
|
||||
|
||||
// Search Navidrome for all songs by this artist
|
||||
let songs = [];
|
||||
try {
|
||||
const { s, t } = mkTok();
|
||||
const r = await fetch(
|
||||
`${ndUrl}/rest/search3?query=${encodeURIComponent(artistKanji)}&artistCount=0&albumCount=0&songCount=500&u=${encodeURIComponent(user)}&t=${t}&s=${s}&v=1.16.1&c=novasonic&f=json`,
|
||||
{ signal: AbortSignal.timeout(15000) }
|
||||
);
|
||||
const d = await r.json();
|
||||
const all = d?.['subsonic-response']?.searchResult3?.song || [];
|
||||
songs = all.filter(sng => sng.artist === artistKanji || sng.albumArtist === artistKanji);
|
||||
} catch (e) {
|
||||
updateJob(jobId, { status: 'error', message: `Navidrome-Suche fehlgeschlagen: ${e.message}` });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!songs.length) {
|
||||
updateJob(jobId, { status: 'done', progress: 100, message: 'Keine Songs gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
log(jobId, `🔍 ${songs.length} Songs gefunden für "${artistKanji}"`);
|
||||
updateJob(jobId, { status: 'running', progress: 10, message: `${songs.length} Songs gefunden…` });
|
||||
|
||||
let done = 0, errors = 0;
|
||||
const safeArtistDir = sanitize(artistSort.split(/[,;&]/)[0].trim());
|
||||
|
||||
for (const ndSong of songs) {
|
||||
try {
|
||||
// Resolve file path
|
||||
let filePath = ndSong.path;
|
||||
if (!filePath) { errors++; continue; }
|
||||
if (!path.isAbsolute(filePath)) filePath = path.join(CONFIG.musicPath, filePath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
const fileName = path.basename(filePath);
|
||||
const titleStem = fileName.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase();
|
||||
const found = _rebasePath(filePath, CONFIG.musicPath)
|
||||
|| _findFileRecursive(CONFIG.musicPath, fileName)
|
||||
|| _findFileByStemOrRomaji(CONFIG.musicPath, titleStem);
|
||||
if (found) filePath = found;
|
||||
else { log(jobId, `⚠ Nicht gefunden: ${path.basename(ndSong.path || '')}`); errors++; continue; }
|
||||
}
|
||||
|
||||
const fileExt = path.extname(filePath);
|
||||
const tmpFile = filePath.slice(0, -fileExt.length) + '.sorttmp' + fileExt;
|
||||
|
||||
// -map_metadata 0 preserves all existing tags; individual -metadata flags override only sort ones
|
||||
await new Promise((resolve, reject) => {
|
||||
const ff = spawn('ffmpeg', [
|
||||
'-i', filePath,
|
||||
'-map', '0:a', '-map', '0:v?',
|
||||
'-c', 'copy', '-map_metadata', '0',
|
||||
'-metadata', `ARTISTSORT=${artistSort}`,
|
||||
'-metadata', `ALBUMARTISTSORT=${artistSort}`,
|
||||
tmpFile, '-y', '-loglevel', 'error',
|
||||
]);
|
||||
let ffErr = '';
|
||||
ff.stderr.on('data', d => ffErr += d.toString());
|
||||
ff.on('error', reject);
|
||||
ff.on('close', code => code === 0 ? resolve() : reject(new Error(`ffmpeg exit ${code}: ${ffErr.slice(-200)}`)));
|
||||
});
|
||||
fs.renameSync(tmpFile, filePath);
|
||||
|
||||
// Move to correct artist/album folder: {musicPath}/{safeArtistDir}/{albumName}/
|
||||
const oldAlbumDir = path.dirname(filePath);
|
||||
const oldArtistDir = path.dirname(oldAlbumDir);
|
||||
const albumName = path.basename(oldAlbumDir);
|
||||
const newAlbumDir = path.join(CONFIG.musicPath, safeArtistDir, albumName);
|
||||
|
||||
if (newAlbumDir !== oldAlbumDir) {
|
||||
fs.mkdirSync(newAlbumDir, { recursive: true });
|
||||
const destFile = path.join(newAlbumDir, path.basename(filePath));
|
||||
if (!fs.existsSync(destFile)) fs.renameSync(filePath, destFile);
|
||||
// Clean up empty old dirs
|
||||
try {
|
||||
if (fs.readdirSync(oldAlbumDir).filter(f => !f.startsWith('.')).length === 0) {
|
||||
fs.rmdirSync(oldAlbumDir);
|
||||
if (fs.readdirSync(oldArtistDir).filter(f => !f.startsWith('.')).length === 0)
|
||||
fs.rmdirSync(oldArtistDir);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
done++;
|
||||
updateJob(jobId, { progress: 10 + Math.round((done / songs.length) * 80), message: `${done}/${songs.length} Songs aktualisiert…` });
|
||||
log(jobId, `✅ ${ndSong.title}`);
|
||||
} catch (e) {
|
||||
log(jobId, `❌ ${ndSong.title}: ${e.message}`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger Navidrome rescan
|
||||
try {
|
||||
const { s, t } = mkTok();
|
||||
await fetch(`${ndUrl}/rest/startScan?u=${encodeURIComponent(user)}&t=${t}&s=${s}&v=1.16.1&c=novasonic&f=json`, { signal: AbortSignal.timeout(5000) });
|
||||
} catch {}
|
||||
|
||||
const msg = errors > 0 ? `${done} aktualisiert, ${errors} Fehler` : `${done} Songs aktualisiert`;
|
||||
updateJob(jobId, { status: 'done', progress: 100, message: msg });
|
||||
})().catch(e => updateJob(jobId, { status: 'error', message: e.message }));
|
||||
});
|
||||
|
||||
app.get('/api/status/:jobId', (req, res) => {
|
||||
const job = jobs.get(req.params.jobId);
|
||||
if (!job) return res.status(404).json({ error: 'Job not found' });
|
||||
@@ -978,7 +1102,7 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
const oldAlbumDir = path.dirname(currentFile);
|
||||
const oldArtistDir = path.dirname(oldAlbumDir);
|
||||
const newAlbumName = romajiAlbum ? sanitize(romajiAlbum) : path.basename(oldAlbumDir);
|
||||
const newArtistName = romajiAlbumArtist ? sanitize(romajiAlbumArtist.split(/[,;&]/)[0].trim()) : path.basename(oldArtistDir);
|
||||
const newArtistName = (albumArtistSort || romajiAlbumArtist) ? sanitize((albumArtistSort || romajiAlbumArtist).split(/[,;&]/)[0].trim()) : path.basename(oldArtistDir);
|
||||
const newDir = path.join(CONFIG.musicPath, newArtistName, newAlbumName);
|
||||
|
||||
if (newDir !== oldAlbumDir) {
|
||||
|
||||
+14
@@ -657,6 +657,20 @@
|
||||
<div id="s-show-romaji" class="toggle" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--accent-dim);display:inline-flex;align-items:center;justify-content:center;font-size:12px">㊙</span>
|
||||
Romaji Korrekturen
|
||||
</h3>
|
||||
<div class="hint" style="margin-bottom:10px">Kanji → korrektes Romaji. Wird in Sort-Tags automatisch übernommen.</div>
|
||||
<div id="romaji-overrides-list" style="display:flex;flex-direction:column;gap:5px;margin-bottom:10px"></div>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<input id="ro-kanji" placeholder="照井順政" style="flex:1;padding:8px 10px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-family:var(--font);font-size:13px;outline:none" onkeydown="if(event.key==='Enter')addRomajiOverrideUI()">
|
||||
<span style="color:var(--text2);flex-shrink:0">→</span>
|
||||
<input id="ro-romaji" placeholder="Terui Yoshimasa" style="flex:1.2;padding:8px 10px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-family:var(--font);font-size:13px;outline:none" onkeydown="if(event.key==='Enter')addRomajiOverrideUI()">
|
||||
<button onclick="addRomajiOverrideUI()" style="flex-shrink:0;padding:8px 12px;border-radius:8px;border:none;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#000;font-family:var(--font);font-size:14px;font-weight:700;cursor:pointer">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🎨</span>
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
# reorganize-music.sh
|
||||
#
|
||||
# Reads metadata tags from audio files and reorganises them into the
|
||||
# NovaSonic folder/filename convention:
|
||||
#
|
||||
# <dest>/<AlbumArtist>/<Album>/<NN - Artist - Title.ext>
|
||||
#
|
||||
# The sanitize(), folder and filename rules exactly mirror
|
||||
# backend/lib/ytdlp.js and backend/lib/metadata.js.
|
||||
#
|
||||
# Usage:
|
||||
# ./reorganize-music.sh <source_dir> <dest_dir> [--move] [--clean]
|
||||
#
|
||||
# <source_dir> Root of the old genre-folder tree (searched recursively)
|
||||
# <dest_dir> Music root to write into (can be the same as source)
|
||||
# --move Actually move files (default: dry-run preview only)
|
||||
# --clean After moving, remove empty directories under <source_dir>
|
||||
#
|
||||
# Requires: ffprobe (part of ffmpeg)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# ── Args ──────────────────────────────────────────────────────────────────────
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Usage: $0 <source_dir> <dest_dir> [--move] [--clean]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE="$(realpath "$1")"
|
||||
DEST="$(realpath "$2")"
|
||||
DO_MOVE=false
|
||||
DO_CLEAN=false
|
||||
for arg in "${@:3}"; do
|
||||
[[ "$arg" == "--move" ]] && DO_MOVE=true
|
||||
[[ "$arg" == "--clean" ]] && DO_CLEAN=true
|
||||
done
|
||||
|
||||
MOVED=0; SKIPPED=0; ERRORS=0
|
||||
|
||||
# ── sanitize(s): mirrors backend/lib/metadata.js sanitize() ──────────────────
|
||||
# Replace filesystem-illegal chars with _, trim whitespace, cap at 100 chars.
|
||||
sanitize() {
|
||||
local s="${1:-Unknown}"
|
||||
s="$(printf '%s' "$s" | sed 's/[\\/:*?"<>|]/_/g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||
printf '%s' "${s:0:100}"
|
||||
}
|
||||
|
||||
# ── Read a single ffprobe tag (returns empty string if absent) ────────────────
|
||||
get_tag() {
|
||||
ffprobe -v quiet \
|
||||
-show_entries "format_tags=$2" \
|
||||
-of "default=noprint_wrappers=1:nokey=1" \
|
||||
"$1" 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
# ── First segment before any artist separator (, ; &) ────────────────────────
|
||||
# Used for the artist *folder* name (album artist, first name only).
|
||||
first_artist() {
|
||||
printf '%s' "$1" | sed 's/[,;&].*//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
# ── Normalise all artists for the filename (; → ,) ───────────────────────────
|
||||
# Mirrors: artistRomaji.replace(/;/g, ',')
|
||||
artists_for_file() {
|
||||
printf '%s' "$1" | sed 's/;[[:space:]]*/\, /g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
# ── Print header ──────────────────────────────────────────────────────────────
|
||||
echo "NovaSonic Music Reorganiser"
|
||||
echo "Source : $SOURCE"
|
||||
echo "Dest : $DEST"
|
||||
if $DO_MOVE; then
|
||||
echo "Mode : MOVE (files will be moved)"
|
||||
else
|
||||
echo "Mode : DRY-RUN (nothing will change — add --move to execute)"
|
||||
fi
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
|
||||
# ── Main loop: every audio file under SOURCE ──────────────────────────────────
|
||||
while IFS= read -r -d '' file; do
|
||||
# ── Extension (lowercase) ───────────────────────────────────────────────
|
||||
filename_base="$(basename "$file")"
|
||||
ext_lower="$(printf '%s' "${filename_base##*.}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
# ── Read tags via ffprobe ────────────────────────────────────────────────
|
||||
raw_title=$(get_tag "$file" "title")
|
||||
raw_artist=$(get_tag "$file" "artist")
|
||||
raw_album_artist=$(get_tag "$file" "album_artist")
|
||||
raw_album=$(get_tag "$file" "album")
|
||||
raw_track=$(get_tag "$file" "track")
|
||||
|
||||
# ── Fallbacks when tags are missing ─────────────────────────────────────
|
||||
# Title: use filename stem (strip extension + leading "NN - " prefix)
|
||||
if [[ -z "$raw_title" ]]; then
|
||||
raw_title="$(printf '%s' "${filename_base%.*}" | sed 's/^[0-9][0-9][[:space:]]*-[[:space:]]*//')"
|
||||
raw_title="$(printf '%s' "$raw_title" | sed 's/[[:space:]]*-[[:space:]]*.*//')" # strip "Artist - " prefix if present
|
||||
[[ -z "$raw_title" ]] && raw_title="Unknown Title"
|
||||
fi
|
||||
|
||||
artist="${raw_artist:-Unknown Artist}"
|
||||
# Album artist preferred for folder; fall back to track artist
|
||||
main_artist="${raw_album_artist:-$artist}"
|
||||
album="${raw_album:-Singles}"
|
||||
|
||||
# ── Track number: "5/12" or "05" → zero-padded "05 - " ──────────────────
|
||||
track_num="$(printf '%s' "$raw_track" | cut -d'/' -f1 | tr -dc '0-9')"
|
||||
if [[ -n "$track_num" && "$track_num" -gt 0 ]] 2>/dev/null; then
|
||||
track_prefix="$(printf '%02d - ' "$track_num")"
|
||||
else
|
||||
track_prefix=""
|
||||
fi
|
||||
|
||||
# ── Build folder path ────────────────────────────────────────────────────
|
||||
# Convention: {dest}/{first_albumArtist}/{album}/
|
||||
dir_artist="$(first_artist "$main_artist")"
|
||||
safe_artist_dir="$(sanitize "$dir_artist")"
|
||||
safe_album_dir="$(sanitize "$album")"
|
||||
target_dir="$DEST/$safe_artist_dir/$safe_album_dir"
|
||||
|
||||
# ── Build filename ───────────────────────────────────────────────────────
|
||||
# Convention: {NN - }Artist1, Artist2 - Title.ext
|
||||
file_artists="$(artists_for_file "$artist")"
|
||||
safe_artist_file="$(sanitize "$file_artists")"
|
||||
safe_title="$(sanitize "$raw_title")"
|
||||
new_filename="${track_prefix}${safe_artist_file} - ${safe_title}.${ext_lower}"
|
||||
|
||||
target="$target_dir/$new_filename"
|
||||
|
||||
# ── Already in the right place? ──────────────────────────────────────────
|
||||
if [[ "$file" == "$target" ]]; then
|
||||
printf ' ✓ already correct %s\n' "$safe_artist_dir/$safe_album_dir/$new_filename"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# ── Conflict: target already exists (don't overwrite) ────────────────────
|
||||
if [[ -e "$target" ]]; then
|
||||
printf ' ⚠ SKIP (exists) %s\n' "$target"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# ── Report what we will / would do ───────────────────────────────────────
|
||||
printf ' → %s/%s/%s\n' "$safe_artist_dir" "$safe_album_dir" "$new_filename"
|
||||
|
||||
if $DO_MOVE; then
|
||||
if mkdir -p "$target_dir" && mv -- "$file" "$target"; then
|
||||
MOVED=$((MOVED + 1))
|
||||
else
|
||||
printf ' ERROR moving: %s\n' "$file" >&2
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
MOVED=$((MOVED + 1))
|
||||
fi
|
||||
|
||||
done < <(find "$SOURCE" -type f \( \
|
||||
-iname "*.mp3" -o -iname "*.flac" -o \
|
||||
-iname "*.m4a" -o -iname "*.opus" -o \
|
||||
-iname "*.ogg" -o -iname "*.wav" \
|
||||
\) -print0 | sort -z)
|
||||
|
||||
# ── Remove empty source dirs (optional) ──────────────────────────────────────
|
||||
if $DO_MOVE && $DO_CLEAN; then
|
||||
echo ""
|
||||
echo "Cleaning empty directories under $SOURCE …"
|
||||
find "$SOURCE" -mindepth 1 -type d -empty -delete 2>/dev/null && \
|
||||
echo " Done." || echo " (nothing to clean or permission denied)"
|
||||
fi
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
if $DO_MOVE; then
|
||||
echo "Done: $MOVED moved, $SKIPPED skipped, $ERRORS errors"
|
||||
else
|
||||
echo "Dry-run: $MOVED would be moved, $SKIPPED already correct or conflicting"
|
||||
echo ""
|
||||
echo "Re-run with --move to execute. Add --clean to also remove empty source dirs."
|
||||
fi
|
||||
+16
@@ -98,6 +98,22 @@ export async function fetchRomajiIndex() {
|
||||
|
||||
export function invalidateRomajiIndex() { _romajiIndex = null; _romajiMap = null; _artistRomajiMap = null; }
|
||||
|
||||
// ─── ROMAJI OVERRIDES ───
|
||||
// Manual kanji → romaji corrections stored in localStorage. Always take priority over auto-conversion.
|
||||
export function getRomajiOverrides() {
|
||||
try { return JSON.parse(localStorage.getItem('ns_romaji_overrides') || '{}'); } catch { return {}; }
|
||||
}
|
||||
export function setRomajiOverride(kanji, romaji) {
|
||||
const o = getRomajiOverrides();
|
||||
if (romaji?.trim()) o[kanji.trim()] = romaji.trim(); else delete o[kanji.trim()];
|
||||
localStorage.setItem('ns_romaji_overrides', JSON.stringify(o));
|
||||
}
|
||||
export function removeRomajiOverride(kanji) {
|
||||
const o = getRomajiOverrides();
|
||||
delete o[kanji];
|
||||
localStorage.setItem('ns_romaji_overrides', JSON.stringify(o));
|
||||
}
|
||||
|
||||
export function dispArtistById(id, name) {
|
||||
if (!S.cfg.showRomaji || !_artistRomajiMap) return name || '';
|
||||
return _artistRomajiMap.get(String(id)) || name || '';
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate,
|
||||
_pickTheme,
|
||||
toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata, deleteSong,
|
||||
addRomajiOverrideUI, removeRomajiOverrideUI,
|
||||
} from './ui.js';
|
||||
|
||||
// ─── CONNECT ───
|
||||
@@ -323,6 +324,7 @@ Object.assign(window, {
|
||||
editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel,
|
||||
toggleMobileSearch, closeMobileSearch, switchSetupTab,
|
||||
toggleNotifPanel, deleteNotif, clearNotifs, _notifNavigate, _pickTheme,
|
||||
addRomajiOverrideUI, removeRomajiOverrideUI,
|
||||
// App
|
||||
doConnect, disconnect,
|
||||
// Audiobookshelf
|
||||
|
||||
@@ -716,6 +716,7 @@ export function nextSong() {
|
||||
// Explicit index prevents desync when JS and native queue pointers diverge.
|
||||
_nsBridge('seekToIndex', ni, 0);
|
||||
_nsBridge('play');
|
||||
}
|
||||
// else: ni = -1 means end of queue with no repeat — let ExoPlayer stop naturally.
|
||||
if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue();
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, formatDur, toast } from './utils.js';
|
||||
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache } from './api.js';
|
||||
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache, getRomajiOverrides, setRomajiOverride, removeRomajiOverride } from './api.js';
|
||||
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
|
||||
// Circular imports — safe (only used inside function bodies)
|
||||
import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt, refreshDisplayMode, updateNpColorMode } from './player.js';
|
||||
@@ -51,6 +51,7 @@ export function openSettings() {
|
||||
document.getElementById('s-show-romaji').classList.toggle('on', !!S.cfg.showRomaji);
|
||||
document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg);
|
||||
_renderThemePicker();
|
||||
_renderRomajiOverrides();
|
||||
document.getElementById('settings-modal').classList.add('open');
|
||||
checkBackend();
|
||||
}
|
||||
@@ -70,6 +71,52 @@ export function _pickTheme(id) {
|
||||
applyTheme(id);
|
||||
_renderThemePicker();
|
||||
}
|
||||
|
||||
function _renderRomajiOverrides() {
|
||||
const list = document.getElementById('romaji-overrides-list');
|
||||
if (!list) return;
|
||||
const entries = Object.entries(getRomajiOverrides());
|
||||
if (!entries.length) {
|
||||
list.innerHTML = '<div style="color:var(--text2);font-size:13px;padding:4px 0">Keine Korrekturen vorhanden</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = entries.map(([k, v]) =>
|
||||
`<div style="display:flex;align-items:center;gap:6px;padding:7px 10px;background:var(--bg2);border-radius:8px">
|
||||
<span style="flex:1;font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(k)}</span>
|
||||
<span style="color:var(--text2);flex-shrink:0">→</span>
|
||||
<span style="flex:1.2;font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(v)}</span>
|
||||
<button onclick="removeRomajiOverrideUI(${JSON.stringify(k)})" style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:18px;line-height:1;padding:0 2px;flex-shrink:0">×</button>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
export async function addRomajiOverrideUI() {
|
||||
const k = (document.getElementById('ro-kanji')?.value || '').trim();
|
||||
const v = (document.getElementById('ro-romaji')?.value || '').trim();
|
||||
if (!k || !v) { toast('⚠ Kanji und Romaji eingeben'); return; }
|
||||
setRomajiOverride(k, v);
|
||||
document.getElementById('ro-kanji').value = '';
|
||||
document.getElementById('ro-romaji').value = '';
|
||||
_renderRomajiOverrides();
|
||||
// Batch-fix existing songs in the library
|
||||
if (!S.cfg.backendUrl || !S.cfg.url || !S.cfg.user || !S.cfg.pass) return;
|
||||
try {
|
||||
const jobId = 'job_' + Date.now();
|
||||
addJob(jobId, `Romaji-Fix: ${v}`, k, 'fix-sort');
|
||||
const res = await fetch(`${S.cfg.backendUrl}/api/fix-artist-sort`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ndUrl: S.cfg.url, user: S.cfg.user, pass: S.cfg.pass, artistKanji: k, artistSort: v, jobId }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
pollJob(jobId);
|
||||
} catch (e) {
|
||||
toast('⚠ Fix-Artist-Sort: ' + e.message);
|
||||
}
|
||||
}
|
||||
export function removeRomajiOverrideUI(kanji) {
|
||||
removeRomajiOverride(kanji);
|
||||
_renderRomajiOverrides();
|
||||
}
|
||||
|
||||
export function closeSettings() { document.getElementById('settings-modal').classList.remove('open'); }
|
||||
export async function confirmDisconnect() {
|
||||
const ok = await showConfirm('Abmelden', 'Wirklich abmelden? Alle lokalen Einstellungen werden gelöscht.', 'Abmelden');
|
||||
@@ -369,14 +416,21 @@ export function toggleInfoEdit() {
|
||||
const p = new URLSearchParams({ ndUrl: S.cfg.url, user: S.cfg.user, pass: S.cfg.pass, songId: _sId });
|
||||
return fetch(`${S.cfg.backendUrl}/api/song-sorttags?${p}`).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
}).then(t => {
|
||||
if (!t) return;
|
||||
const _jp = /[ぁ-んァ-ン一-龯]/;
|
||||
// Only use file sort tags if they are non-Japanese (romaji/ASCII) — Japanese sort tags
|
||||
// in the file are incorrect and should be ignored in favour of the romaji index value.
|
||||
if (t.titleSort && !_jp.test(t.titleSort)) document.getElementById('ie-titlesort').value = t.titleSort;
|
||||
if (t.artistSort && !_jp.test(t.artistSort)) document.getElementById('ie-artistsort').value = t.artistSort;
|
||||
if (t.albumArtistSort && !_jp.test(t.albumArtistSort)) document.getElementById('ie-albumartistsort').value = t.albumArtistSort;
|
||||
if (t.albumSort && !_jp.test(t.albumSort)) document.getElementById('ie-albumsort').value = t.albumSort;
|
||||
if (t) {
|
||||
const _jp = /[ぁ-んァ-ン一-龯]/;
|
||||
// Only use file sort tags if they are non-Japanese (romaji/ASCII) — Japanese sort tags
|
||||
// in the file are incorrect and should be ignored in favour of the romaji index value.
|
||||
if (t.titleSort && !_jp.test(t.titleSort)) document.getElementById('ie-titlesort').value = t.titleSort;
|
||||
if (t.artistSort && !_jp.test(t.artistSort)) document.getElementById('ie-artistsort').value = t.artistSort;
|
||||
if (t.albumArtistSort && !_jp.test(t.albumArtistSort)) document.getElementById('ie-albumartistsort').value = t.albumArtistSort;
|
||||
if (t.albumSort && !_jp.test(t.albumSort)) document.getElementById('ie-albumsort').value = t.albumSort;
|
||||
}
|
||||
// Manual overrides — highest priority, always applied last.
|
||||
const ov = getRomajiOverrides();
|
||||
if (s.artist && ov[s.artist]) { document.getElementById('ie-artistsort').value = ov[s.artist]; document.getElementById('ie-albumartistsort').value = ov[s.artist]; }
|
||||
if (s.albumArtist && ov[s.albumArtist]) document.getElementById('ie-albumartistsort').value = ov[s.albumArtist];
|
||||
if (s.title && ov[s.title]) document.getElementById('ie-titlesort').value = ov[s.title];
|
||||
if (s.album && ov[s.album]) document.getElementById('ie-albumsort').value = ov[s.album];
|
||||
}).catch(() => {});
|
||||
|
||||
// Art preview — use existing cover
|
||||
@@ -570,11 +624,15 @@ export async function executeDownload() {
|
||||
if(!S.downloadTarget || !S.cfg.backendUrl) return;
|
||||
closeDlModal();
|
||||
const jobId = 'job_' + Date.now();
|
||||
const ov = getRomajiOverrides();
|
||||
const _artist = S.downloadTarget.artistName || 'Unknown Artist';
|
||||
const _title = S.downloadTarget.trackName || S.downloadTarget.collectionName || 'Unknown';
|
||||
const _album = S.downloadTarget.collectionName || '';
|
||||
const payload = {
|
||||
type: S.dlType,
|
||||
artist: S.downloadTarget.artistName || 'Unknown Artist',
|
||||
title: S.downloadTarget.trackName || S.downloadTarget.collectionName || 'Unknown',
|
||||
album: S.downloadTarget.collectionName || '',
|
||||
artist: _artist,
|
||||
title: _title,
|
||||
album: _album,
|
||||
artworkUrl: S.downloadTarget.artworkUrl100 || S.downloadTarget.artworkUrl60 || '',
|
||||
itunesId: S.downloadTarget.trackId || S.downloadTarget.collectionId || null,
|
||||
collectionId: S.downloadTarget.collectionId || null,
|
||||
@@ -584,6 +642,9 @@ export async function executeDownload() {
|
||||
discNum: S.downloadTarget.discNumber || undefined,
|
||||
discTotal: S.downloadTarget.discCount || undefined,
|
||||
durationMs: S.downloadTarget.trackTimeMillis || undefined,
|
||||
...(ov[_artist] && { artistSort: ov[_artist], albumArtistSort: ov[_artist] }),
|
||||
...(ov[_title] && { titleSort: ov[_title] }),
|
||||
...(ov[_album] && { albumSort: ov[_album] }),
|
||||
};
|
||||
addJob(jobId, payload.title, payload.artist, payload.type, payload.album);
|
||||
try {
|
||||
@@ -604,11 +665,23 @@ export async function executeSpotifyDownload({ type, title, artist, artworkUrl,
|
||||
if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; }
|
||||
const jobId = 'job_' + Date.now();
|
||||
addJob(jobId, title, artist, type);
|
||||
const ov = getRomajiOverrides();
|
||||
const enrichedTracks = (tracks || []).map(t => {
|
||||
const a = t.artistName || '';
|
||||
const n = t.trackName || '';
|
||||
const al = t.collectionName || '';
|
||||
return {
|
||||
...t,
|
||||
...(ov[a] && { artistSort: ov[a], albumArtistSort: ov[a] }),
|
||||
...(ov[n] && { titleSort: ov[n] }),
|
||||
...(ov[al] && { albumSort: ov[al] }),
|
||||
};
|
||||
});
|
||||
try {
|
||||
const res = await fetch(`${S.cfg.backendUrl}/api/download`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jobId, type, title, artist, artworkUrl, tracks }),
|
||||
body: JSON.stringify({ jobId, type, title, artist, artworkUrl, tracks: enrichedTracks }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
pollJob(jobId);
|
||||
|
||||
Reference in new issue
Block a user