feat: 粘贴网易云 / QQ 音乐歌单分享链接导入为自建歌单
- POST /api/playlists/import:resolveLink 识别歌单 / 专辑链接,拉取全部曲目后建歌单,简介注明来源与原简介 - 歌单页新增「导入歌单」;上游只返回部分曲目时提示实际获取数 - 网易云歌单详情补上 description Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,7 @@ export class NeteaseProvider implements MusicProvider {
|
||||
name: data.name,
|
||||
cover: data.coverImgUrl,
|
||||
creator: data.creator?.nickname,
|
||||
description: data.description || undefined,
|
||||
trackCount: data.trackCount ?? data.tracks.length,
|
||||
tracks: data.tracks.map((t: any) => this.toSummary(t)),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// 自建歌单:在线曲目(source + id)与本地曲库文件(相对路径)可以混排在同一个歌单里
|
||||
import fs from 'node:fs';
|
||||
import { getProvider, resolveLink } from './core/providers/index.ts';
|
||||
import type { Source } from './core/types.ts';
|
||||
import { db } from './db.ts';
|
||||
import { bus } from './events.ts';
|
||||
@@ -255,3 +256,24 @@ export function reorderItems(id: number, itemIds: unknown): void {
|
||||
tx(() => ordered.forEach((itemId, i) => update.run(i, itemId)));
|
||||
changed(id);
|
||||
}
|
||||
|
||||
// ---------- 从分享链接导入 ----------
|
||||
|
||||
const SOURCE_NAME: Record<Source, string> = { netease: '网易云音乐', qq: 'QQ 音乐' };
|
||||
|
||||
/** 把网易云 / QQ 音乐的歌单(或专辑)分享链接导入为新歌单;total 大于 fetched 说明上游只返回了部分曲目 */
|
||||
export async function importPlaylist(url: unknown): Promise<{ playlist: PlaylistSummary; fetched: number; total: number }> {
|
||||
const ref = await resolveLink(text(url, 2000));
|
||||
if (!ref || (ref.kind !== 'playlist' && ref.kind !== 'album')) throw httpError(400, '请粘贴网易云或 QQ 音乐的歌单 / 专辑分享链接');
|
||||
const provider = getProvider(ref.source);
|
||||
const detail = ref.kind === 'playlist' ? await provider.getPlaylist(ref.id) : await provider.getAlbum(ref.id);
|
||||
if (!detail?.tracks.length) throw httpError(404, ref.kind === 'playlist' ? '歌单不存在或没有歌曲' : '专辑不存在或没有歌曲');
|
||||
const isList = 'trackCount' in detail;
|
||||
const by = isList ? detail.creator : detail.artist;
|
||||
const origin = `导入自${SOURCE_NAME[ref.source]}${isList ? '歌单' : '专辑'}${by ? ` · ${by}` : ''}`;
|
||||
const description = [origin, detail.description?.trim()].filter(Boolean).join('\n\n');
|
||||
const items = detail.tracks.map((t) => ({ source: t.source, id: t.id, name: t.name, artists: t.artists, album: t.album, albumId: t.albumId, cover: t.cover, duration: t.duration }));
|
||||
const playlist = createPlaylist(detail.name, description, items);
|
||||
const fetched = detail.tracks.length;
|
||||
return { playlist, fetched, total: isList ? Math.max(detail.trackCount, fetched) : fetched };
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
zipStream,
|
||||
} from '../library.ts';
|
||||
import { findInLibrary, libraryIndexInfo, rebuildLibraryIndex } from '../libraryIndex.ts';
|
||||
import { addItems, createPlaylist, deletePlaylist, getPlaylist, listPlaylists, removeItems, reorderItems, updatePlaylist } from '../playlists.ts';
|
||||
import { addItems, createPlaylist, deletePlaylist, getPlaylist, importPlaylist, listPlaylists, removeItems, reorderItems, updatePlaylist } from '../playlists.ts';
|
||||
import { queue, type EnqueueItem } from '../queue.ts';
|
||||
import { getSettings, updateSettings } from '../settings.ts';
|
||||
import { proxyStream } from '../stream.ts';
|
||||
@@ -301,6 +301,10 @@ api.post('/playlists', (ctx) => {
|
||||
ctx.body = createPlaylist(name, description, items);
|
||||
});
|
||||
|
||||
api.post('/playlists/import', async (ctx) => {
|
||||
ctx.body = await importPlaylist(body(ctx).url);
|
||||
});
|
||||
|
||||
api.get('/playlists/:id', async (ctx) => {
|
||||
ctx.body = await getPlaylist(playlistId(ctx));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user