diff --git a/index.html b/index.html index ee1cfff..12a726f 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/package.json b/package.json index e8bc371..2b5e815 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@family-flix/admin", "private": true, - "version": "1.8.0", + "version": "1.9.0", "scripts": { "dev": "vite --port 3003 --host", "build": "tsc && vite build", diff --git a/src/domains/drive/files.ts b/src/domains/drive/files.ts index aeee3ca..fb89f0c 100644 --- a/src/domains/drive/files.ts +++ b/src/domains/drive/files.ts @@ -1,14 +1,16 @@ +/** + * @todo 如果删除当前选中的文件夹,子文件夹在视图上也要同步移除 + */ import { Handler } from "mitt"; import { BaseDomain } from "@/domains/base"; import { ListCore } from "@/domains/list"; import { RequestCore } from "@/domains/request"; -import { FileType } from "@/constants"; import { ScrollViewCore } from "@/domains/ui"; +import { FileType } from "@/constants"; import { fetchDriveFiles, deleteFileOfDrive, renameFileOfDrive } from "./services"; import { AliyunFilePath, AliyunDriveFile } from "./types"; -import { Result } from "@/types"; type FileColumn = { list: ListCore; @@ -205,29 +207,36 @@ export class AliyunDriveFilesCore extends BaseDomain { position: [number, number]; onLoading?: (loading: boolean) => void; onFailed?: (error: Error) => void; - onSuccess?: (data: { job_id: string; deleteFile: () => void }) => void; + onSuccess?: (options: { job_id?: string; deleteFile: () => void }) => void; }) { const { file, position, onLoading, onFailed, onSuccess } = options; const [columnIndex, fileIndex] = position; const folderColumns = this.folderColumns; + function delete_file() { + const column = folderColumns[columnIndex]; + column.list.deleteItem((f) => { + if (f.file_id === file.file_id) { + return true; + } + return false; + }); + } const folderDeletingRequest = new RequestCore(deleteFileOfDrive, { onLoading, onFailed, onSuccess: (data) => { if (onSuccess) { - onSuccess({ - job_id: data.job_id, - // @todo 这个实现很糟糕 - deleteFile: () => { - const column = folderColumns[columnIndex]; - column.list.deleteItem((f) => { - if (f.file_id === file.file_id) { - return true; + onSuccess( + data + ? // @todo 这个实现很糟糕 + { + job_id: data.job_id, + deleteFile: delete_file, + } + : { + deleteFile: delete_file, } - return false; - }); - }, - }); + ); } }, }); diff --git a/src/pages/drive/profile.tsx b/src/pages/drive/profile.tsx index ded2d7d..e96ff45 100644 --- a/src/pages/drive/profile.tsx +++ b/src/pages/drive/profile.tsx @@ -12,6 +12,8 @@ import { SelectionCore } from "@/domains/cur"; import { ViewComponent } from "@/types"; import { FileType } from "@/constants"; import { createJob, homeLayout } from "@/store"; +import { RequestCore } from "@/domains/request"; +import { sync_folder } from "@/services"; export const DriveProfilePage: ViewComponent = (props) => { const { app, view } = props; @@ -99,20 +101,18 @@ export const DriveProfilePage: ViewComponent = (props) => { text: ["删除文件失败", error.message], }); }, - onSuccess(data) { + onSuccess(opt) { app.tip({ - text: ["开始删除,需要等待一会"], + text: ["删除成功"], }); + opt.deleteFile(); folderDeletingConfirmDialog.hide(); fileSelect.clear(); + if (!opt.job_id) { + return; + } createJob({ - job_id: data.job_id, - onFinish() { - app.tip({ - text: ["完成删除"], - }); - data.deleteFile(); - }, + job_id: opt.job_id, }); }, }); @@ -179,6 +179,11 @@ export const DriveProfilePage: ViewComponent = (props) => { fileMenu.hide(); }, }); + const syncFolderRequest = new RequestCore(sync_folder, { + // onLoading(loading) { + // folderSyncItem.disable(); + // }, + }); const folderDeletingItem = new MenuItemCore({ label: "删除", async onClick() { @@ -190,7 +195,7 @@ export const DriveProfilePage: ViewComponent = (props) => { } const [file] = driveFileManage.virtualSelectedFolder; fileSelect.select(driveFileManage.virtualSelectedFolder); - folderDeletingConfirmDialog.setTitle(`确认删除 '${file.name}' 吗?`); + folderDeletingConfirmDialog.setTitle(`删除文件`); folderDeletingConfirmDialog.show(); fileMenu.hide(); }, @@ -198,6 +203,43 @@ export const DriveProfilePage: ViewComponent = (props) => { const linkSharedFileItem = new MenuItemCore({ label: "关联分享资源", }); + const folderSyncItem = new MenuItemCore({ + label: "同步资源", + async onClick() { + if (!driveFileManage.virtualSelectedFolder) { + app.tip({ + text: ["请选择要同步的文件夹"], + }); + return; + } + const [file] = driveFileManage.virtualSelectedFolder; + if (file.type === FileType.File) { + app.tip({ + text: ["请选择文件夹进行同步"], + }); + return; + } + folderSyncItem.disable(); + const r = await syncFolderRequest.run({ + file_id: file.file_id, + drive_id: view.params.id, + }); + folderSyncItem.enable(); + if (r.error) { + app.tip({ + text: ["同步失败", r.error.message], + }); + return; + } + fileMenu.hide(); + app.tip({ + text: ["开始同步"], + }); + createJob({ + job_id: r.data.job_id, + }); + }, + }); const fileMenu = new DropdownMenuCore({ side: "right", align: "start", @@ -219,6 +261,7 @@ export const DriveProfilePage: ViewComponent = (props) => { // router.push(`/play/${file.file_id}`); }, }), + folderSyncItem, folderDeletingItem, ], onHidden() { diff --git a/src/pages/movie/index.tsx b/src/pages/movie/index.tsx index ccb8f8e..665ef18 100644 --- a/src/pages/movie/index.tsx +++ b/src/pages/movie/index.tsx @@ -178,7 +178,8 @@ export const MovieManagePage: ViewComponent = (props) => {
-
+
+

{name}

diff --git a/src/pages/movie/profile.tsx b/src/pages/movie/profile.tsx index 9bea3a7..c07aea4 100644 --- a/src/pages/movie/profile.tsx +++ b/src/pages/movie/profile.tsx @@ -4,14 +4,21 @@ import { For, Show, createSignal, onMount } from "solid-js"; import { ArrowLeft } from "lucide-solid"; -import { MovieProfile, delete_movie, fetch_movie_profile, update_movie_profile } from "@/services"; +import { + MovieProfile, + delete_movie, + fetch_movie_profile, + parse_video_file_name, + update_movie_profile, + upload_subtitle_for_movie, +} from "@/services"; import { Button, Dialog, Skeleton, LazyImage, ScrollView, Input } from "@/components/ui"; -import { TMDBSearcherDialog } from "@/components/TMDBSearcher/dialog"; -import { TMDBSearcherDialogCore } from "@/components/TMDBSearcher/store"; +import { TMDBSearcherDialog, TMDBSearcherDialogCore } from "@/components/TMDBSearcher"; import { DialogCore, ButtonCore, ScrollViewCore, InputCore } from "@/domains/ui"; import { RequestCore } from "@/domains/request"; import { ViewComponent } from "@/types"; import { appendAction, homeLayout, mediaPlayingPage, rootView } from "@/store"; +import { SelectionCore } from "@/domains/cur"; export const MovieProfilePage: ViewComponent = (props) => { const { app, view } = props; @@ -37,18 +44,102 @@ export const MovieProfilePage: ViewComponent = (props) => { profileRequest.reload(); }, }); + const filenameParseRequest = new RequestCore(parse_video_file_name, { + onLoading(loading) { + subtitleUploadDialog.okBtn.setLoading(loading); + }, + }); + const uploadRequest = new RequestCore(upload_subtitle_for_movie, { + onLoading(loading) { + subtitleUploadDialog.okBtn.setLoading(loading); + }, + onSuccess() { + app.tip({ + text: ["字幕上传成功"], + }); + subtitleUploadDialog.hide(); + }, + onFailed(error) { + app.tip({ + text: ["字幕上传失败", error.message], + }); + }, + }); + const subtitleValues = new SelectionCore<{ drive_id: string; lang: string; file: File }>(); const subtitleUploadInput = new InputCore({ defaultValue: [], placeholder: "上传字幕文件", type: "file", - onChange(v) { - console.log(v); + async onChange(v) { + const file = v[0]; + if (!file) { + return; + } + if (!profileRequest.response) { + app.tip({ + text: ["请等待详情加载完成"], + }); + return; + } + if (profileRequest.response.sources.length === 0) { + app.tip({ + text: ["必须包含至少一个视频源"], + }); + return; + } + const { name } = file; + const r = await filenameParseRequest.run({ name, keys: ["subtitle_lang"] }); + if (r.error) { + app.tip({ + text: ["文件名解析失败"], + }); + return; + } + const { subtitle_lang } = r.data; + if (!subtitle_lang) { + app.tip({ + text: ["文件名中没有解析出字幕语言"], + }); + return; + } + const sources = profileRequest.response.sources; + const reference_id = sources[0].drive.id; + // 使用 every 方法遍历数组,检查每个元素的 drive.id 是否和参考 id 相同 + const all_ids_equal = sources.every((source) => source.drive.id === reference_id); + if (!all_ids_equal) { + app.tip({ + text: ["视频源在多个云盘内,请手动选择上传至哪个云盘"], + }); + return; + } + subtitleValues.select({ + drive_id: reference_id, + file, + lang: subtitle_lang, + }); + }, + }); + const subtitleUploadBtn = new ButtonCore({ + onClick() { + subtitleUploadDialog.show(); }, }); const subtitleUploadDialog = new DialogCore({ title: "上传字幕", onOk() { - // 开始上传字幕 + if (!subtitleValues.value) { + app.tip({ + text: ["请先上传字幕文件"], + }); + return; + } + const { drive_id, lang, file } = subtitleValues.value; + uploadRequest.run({ + movie_id: view.params.id, + drive_id, + lang, + file, + }); }, }); const movieDeletingBtn = new ButtonCore({ @@ -115,7 +206,7 @@ export const MovieProfilePage: ViewComponent = (props) => { const [profile, setProfile] = createSignal(null); - view.onShow(() => { + onMount(() => { const { id } = view.params; profileRequest.run({ movie_id: id }); }); @@ -181,13 +272,18 @@ export const MovieProfilePage: ViewComponent = (props) => {

- + +
可播放源
{(source) => { - const { file_id, file_name, parent_paths } = source; + const { file_id, file_name, parent_paths, drive } = source; return (
{ }} >
-
+
{parent_paths}/{file_name}
diff --git a/src/pages/tv/profile.tsx b/src/pages/tv/profile.tsx index 855dda8..7816713 100644 --- a/src/pages/tv/profile.tsx +++ b/src/pages/tv/profile.tsx @@ -148,13 +148,54 @@ export const TVProfilePage: ViewComponent = (props) => { }, }); const uploadRequest = new RequestCore(upload_subtitle_for_episode, { + onLoading(loading) { + subtitleUploadDialog.okBtn.setLoading(loading); + }, onSuccess() { app.tip({ text: ["字幕上传成功"], }); + subtitleUploadDialog.hide(); + }, + onFailed(error) { + app.tip({ + text: ["字幕上传失败", error.message], + }); }, }); const filenameParseRequest = new RequestCore(parse_video_file_name); + const subtitleValues = new SelectionCore<{ + episode_text: string; + season_text: string; + drive_id: string; + lang: string; + file: File; + }>(); + const subtitleUploadDialog = new DialogCore({ + title: "上传字幕", + onOk() { + if (!subtitleValues.value) { + app.tip({ + text: ["请先上传字幕文件"], + }); + return; + } + const { drive_id, lang, episode_text, season_text, file } = subtitleValues.value; + uploadRequest.run({ + tv_id: view.params.id, + season_text, + episode_text, + drive_id, + lang, + file, + }); + }, + }); + const subtitleUploadBtn = new ButtonCore({ + onClick() { + subtitleUploadDialog.show(); + }, + }); const subtitleUploadInput = new InputCore({ defaultValue: [], type: "file", @@ -177,7 +218,7 @@ export const TVProfilePage: ViewComponent = (props) => { }); return; } - const { subtitle_lang, episode: episode_text } = r.data; + const { subtitle_lang, episode: episode_text, season: season_text } = r.data; if (!subtitle_lang) { app.tip({ text: ["文件名中没有解析出字幕语言"], @@ -211,12 +252,13 @@ export const TVProfilePage: ViewComponent = (props) => { }); return; } - uploadRequest.run({ - episode_id: matched_episode.id, - drive_id: sources[0].drive.id, + subtitleValues.select({ + episode_text, + season_text, + drive_id: reference_id, file, + lang: subtitle_lang, }); - console.log(r.data, subtitle_lang, matched_episode); }, }); const tvDeleteBtn = new ButtonCore({ @@ -286,8 +328,12 @@ export const TVProfilePage: ViewComponent = (props) => { const [profile, setProfile] = createSignal(null); const [curEpisodeResponse, setCurEpisodeResponse] = createSignal(curEpisodeList.response); + const [parsedSubtitle, setParsedSubtitle] = createSignal(subtitleValues.value); const [curSeason, setCurSeason] = createSignal(curSeasonSelector.value); + subtitleValues.onStateChange((nextState) => { + setParsedSubtitle(nextState); + }); curSeasonSelector.onStateChange((nextState) => { setCurSeason(nextState); }); @@ -370,9 +416,7 @@ export const TVProfilePage: ViewComponent = (props) => { -
- -
+
@@ -484,6 +528,27 @@ export const TVProfilePage: ViewComponent = (props) => {
请仅在需要重新索引关联的文件时进行删除操作
+ + +
+ {/*
+
名称
+
{parsedSubtitle()?.episode_text}
+
*/} +
+
+
{parsedSubtitle()?.season_text}
+
+
+
+
{parsedSubtitle()?.episode_text}
+
+
+
语言
+
{parsedSubtitle()?.lang}
+
+
+
); diff --git a/src/services/index.ts b/src/services/index.ts index b8e72c9..459d744 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -9,6 +9,7 @@ import { request } from "@/utils/request"; import { JSONObject, ListResponse, RequestedResource, Result, Unpacked, UnpackedResult } from "@/types"; import { EpisodeResolutionTypeTexts, EpisodeResolutionTypes } from "@/domains/tv/constants"; import { ReportTypeTexts, ReportTypes } from "@/constants"; +import { query_stringify } from "@/utils"; /** * 获取电视剧列表 @@ -1098,9 +1099,15 @@ export async function fetch_movie_profile(body: { movie_id: string }) { air_date: string; tmdb_id: number; sources: { + id: string; file_id: string; - parent_paths: string; file_name: string; + parent_paths: string; + drive: { + id: string; + name: string; + avatar: string; + }; }[]; }>(`/api/admin/movie/${movie_id}`); return r; @@ -1111,11 +1118,36 @@ export function upload_file(body: FormData) { return request.post("/api/admin/upload", body); } -export function upload_subtitle_for_episode(params: { episode_id: string; drive_id: string; file: File }) { - const { episode_id, drive_id, file } = params; +export function upload_subtitle_for_episode(params: { + tv_id: string; + season_text: string; + episode_text: string; + lang: string; + drive_id: string; + file: File; +}) { + const { tv_id, drive_id, lang, season_text, episode_text, file } = params; const body = new FormData(); body.append("file", file); - return request.post(`/api/admin/episode/${episode_id}/subtitle/add?drive_id=${drive_id}`, body); + body.append("drive", drive_id); + body.append("lang", lang); + body.append("season_text", season_text); + body.append("episode_text", episode_text); + const search = query_stringify({ + tv_id, + drive_id, + lang, + season_text, + episode_text, + }); + return request.post(`/api/admin/episode/subtitle/add?${search}`, body); +} + +export function upload_subtitle_for_movie(params: { movie_id: string; drive_id: string; lang: string; file: File }) { + const { movie_id, drive_id, lang, file } = params; + const body = new FormData(); + body.append("file", file); + return request.post(`/api/admin/movie/${movie_id}/subtitle/add?drive_id=${drive_id}&lang=${lang}`, body); } export function notify_test(values: { text: string; token: string }) { @@ -1131,3 +1163,8 @@ export function update_settings(values: Partial<{ push_deer_token: string }>) { const { push_deer_token } = values; return request.post(`/api/admin/settings/update`, { push_deer_token }); } + +export function sync_folder(values: { drive_id: string; file_id: string }) { + const { drive_id, file_id } = values; + return request.get<{ job_id: string }>(`/api/admin/file/${file_id}/sync`, { drive_id }); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 1728063..f17c40b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -88,6 +88,11 @@ export function update(arr: T[], index: number, nextItem: T) { return [...arr.slice(0, index), nextItem, ...arr.slice(index + 1)]; } +/** + * 将对象转成 search 字符串,前面不带 ? + * @param query + * @returns + */ export function query_stringify(query: Record) { return Object.keys(query) .filter((key) => {