From cf69b467087d8f195dae0b485b83ad16af06117f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=BD=E5=9F=B9=E8=80=85?= Date: Tue, 31 Dec 2024 22:31:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0Emby=E5=AA=92?= =?UTF-8?q?=E4=BD=93=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=94=AF=E6=8C=81(Beta)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要功能:在参数配置页添加媒体服务器(Emby)之后,可以在搜索页面的电影卡片下方显示服务器中已存在的媒体信息。 PS:给2024年画上圆满的句号,2025诸事顺意! --- public/assets/media-server/emby.svg | 1 + resource/i18n/en.json | 34 ++- resource/i18n/zh-CN.json | 34 ++- src/background/controller.ts | 10 + src/background/mediaServerManager.ts | 58 +++++ src/background/plugins/Emby.ts | 127 +++++++++ src/interface/common.ts | 18 +- src/interface/enum.ts | 14 +- .../components/MediaServerInfoCard.vue | 234 +++++++++++++++++ src/options/components/MovieInfoCard.vue | 16 ++ src/options/components/Navigation.vue | 15 +- src/options/router.ts | 7 +- src/options/store.ts | 69 ++++- .../views/settings/MediaServers/Emby/Edit.vue | 73 ++++++ .../settings/MediaServers/Emby/Editor.vue | 155 +++++++++++ .../views/settings/MediaServers/Index.vue | 244 ++++++++++++++++++ src/service/api.ts | 4 +- src/service/extension.ts | 4 +- 18 files changed, 1091 insertions(+), 26 deletions(-) create mode 100644 public/assets/media-server/emby.svg create mode 100644 src/background/mediaServerManager.ts create mode 100644 src/background/plugins/Emby.ts create mode 100644 src/options/components/MediaServerInfoCard.vue create mode 100644 src/options/views/settings/MediaServers/Emby/Edit.vue create mode 100644 src/options/views/settings/MediaServers/Emby/Editor.vue create mode 100644 src/options/views/settings/MediaServers/Index.vue diff --git a/public/assets/media-server/emby.svg b/public/assets/media-server/emby.svg new file mode 100644 index 000000000..498fb314e --- /dev/null +++ b/public/assets/media-server/emby.svg @@ -0,0 +1 @@ +Emby \ No newline at end of file diff --git a/resource/i18n/en.json b/resource/i18n/en.json index 178cd2be8..ddb875b8e 100644 --- a/resource/i18n/en.json +++ b/resource/i18n/en.json @@ -88,7 +88,8 @@ "downloadPaths": "Download Paths", "searchSolution": "Search Solution", "backup": "Backup & Restore", - "permissions": "Permissions" + "permissions": "Permissions", + "mediaServers": "Media Servers" }, "thanks": { "title": "Thanks", @@ -849,6 +850,37 @@ "action": "Action" } } + }, + "mediaServers": { + "index": { + "title": "Media Servers", + "subTitle": "Media servers are currently only used to determine if the searched resource already exists.", + "add": "Add", + "remove": "Remove", + "clear": "Clear", + "itemDuplicate": "The name already exists.", + "removeConfirm": "Are you sure you want to remove this media server?", + "removeConfirmTitle": "Remove Confirmation", + "clearConfirm": "Are you sure you want to remove all media servers?", + "removeSelectedConfirm": "Are you sure you want to remove the selected media servers?", + "yes": "Yes", + "no": "No", + "headers": { + "name": "Name", + "enabled": "Enabled", + "type": "Type", + "address": "Server Address", + "action": "Action" + } + }, + "editor": { + "type": "Server Type", + "name": "Service Name", + "address": "Server Address", + "addressTip": "Complete server address (including port), e.g.: http://192.168.1.1:5000/", + "apiKey": "API KEY", + "test": "Test if the server is connectable" + } } }, "statistic": { diff --git a/resource/i18n/zh-CN.json b/resource/i18n/zh-CN.json index 473cf0961..910ffdde3 100644 --- a/resource/i18n/zh-CN.json +++ b/resource/i18n/zh-CN.json @@ -85,7 +85,8 @@ "downloadPaths": "下载目录设置", "searchSolution": "搜索方案", "backup": "参数备份与恢复", - "permissions": "权限设置" + "permissions": "权限设置", + "mediaServers": "媒体服务器" }, "thanks": { "title": "鸣谢", @@ -844,6 +845,37 @@ "action": "操作" } } + }, + "mediaServers": { + "index": { + "title": "媒体服务器", + "subTitle": "媒体服务器当前仅用于判断搜索资源是否已存在。", + "add": "新增", + "remove": "删除", + "clear": "清除", + "itemDuplicate": "该名称已存在", + "removeConfirm": "确认要删除这个媒体服务器吗?", + "removeConfirmTitle": "删除确认", + "clearConfirm": "确认要删除所有媒体服务器吗?", + "removeSelectedConfirm": "确认要删除已选中的媒体服务器吗?", + "yes": "是", + "no": "否", + "headers": { + "name": "名称", + "enabled": "是否启用", + "type": "类型", + "address": "服务器地址", + "action": "操作" + } + }, + "editor": { + "type": "服务器类型", + "name": "服务名称", + "address": "服务器地址", + "addressTip": "完整的服务器地址(含端口),如:http://192.168.1.1:5000/", + "apiKey": "API KEY", + "test": "测试服务器是否可连接" + } } }, "statistic": { diff --git a/src/background/controller.ts b/src/background/controller.ts index f8323fd38..0c6bea725 100644 --- a/src/background/controller.ts +++ b/src/background/controller.ts @@ -29,6 +29,7 @@ import { User } from "./user"; import { MovieInfoService } from "@/service/movieInfoService"; import { remote as parseTorrentRemote } from "parse-torrent"; import { PPF } from "@/service/public"; +import { MediaServerManager } from "./mediaServerManager"; type Service = PTPlugin; export default class Controller { @@ -46,6 +47,7 @@ export default class Controller { public searcher: Searcher = new Searcher(this.service); public userService: User = new User(this.service); public movieInfoService = new MovieInfoService(); + public mediaServerManager = new MediaServerManager(); public clientController: ClientController = new ClientController(); public isInitialized: boolean = false; @@ -1367,6 +1369,14 @@ export default class Controller { return this.service.config.testBackupServerConnectivity(options); } + public testMediaServerConnectivity(options: any): Promise { + return this.mediaServerManager.ping(options); + } + + public getMediaFromMediaServer(options: any): Promise { + return this.mediaServerManager.getMediaFromMediaServer(options.server, options.imdbId); + } + public createSearchResultSnapshot(options: any): Promise { return this.service.searchResultSnapshot.add(options); } diff --git a/src/background/mediaServerManager.ts b/src/background/mediaServerManager.ts new file mode 100644 index 000000000..81d1c6afa --- /dev/null +++ b/src/background/mediaServerManager.ts @@ -0,0 +1,58 @@ +import { EMediaServerType, IMediaServer } from "@/interface/common"; +import { Emby } from "./plugins/Emby"; + +export class MediaServerManager { + private servers: any = {}; + + + private getServer(options: IMediaServer) { + let server = this.servers[options.id]; + + if (server) { + return server; + } + + switch (options.type) { + case EMediaServerType.Emby: + server = new Emby(options); + break; + + default: + break; + } + + if (server) { + this.servers[options.id] = server; + } + + return server; + } + + public reset() { + for (const item of this.servers) { + this.servers[item] = undefined; + delete this.servers[item]; + } + this.servers = {}; + } + + public async ping(options: IMediaServer) { + let server = this.getServer(options); + + if (server) { + return server.ping(); + } + + return false; + } + + public async getMediaFromMediaServer(options: IMediaServer, imdbId: string) { + let server = this.getServer(options); + + if (server) { + return server.getMediaFromMediaServer(imdbId); + } + + return false; + } +} \ No newline at end of file diff --git a/src/background/plugins/Emby.ts b/src/background/plugins/Emby.ts new file mode 100644 index 000000000..fe786ee77 --- /dev/null +++ b/src/background/plugins/Emby.ts @@ -0,0 +1,127 @@ +import { IMediaServer } from "@/interface/common"; +export type Dictionary = { [key: string]: T }; + +export class Emby { + public serverURL: string = ""; + + private API: any = { + methods: { + findFromIMDb: 'Items?Recursive=true&Fields=Path,Size,OfficialRating,MediaSources&AnyProviderIdEquals=imdb.$imdbId$', + getSystemInfo: 'System/Info' + } + } + + constructor(public options: IMediaServer) { + this.serverURL = this.options.address; + if (this.serverURL.substr(-1) !== "/") { + this.serverURL += "/"; + } + } + + /** + * 替换指定的字符串列表 + * @param source + * @param maps + */ + public replaceKeys( + source: string, + maps: Dictionary, + prefix: string = "" + ): string { + if (!source) { + return source; + } + let result: string = source; + + for (const key in maps) { + if (maps.hasOwnProperty(key)) { + const value = maps[key]; + let search = "$" + key + "$"; + if (prefix) { + search = `$${prefix}.${key}$`; + } + result = result.replace(search, value); + } + } + return result; + } + + /** + * 指定指定的API + * @param method + * @param data + * @returns + */ + public async execAPI(method = '', data: any = {}) { + let m: any; + let methods = method.split("."); + + if (methods.length == 1) { + m = this.API.methods[method] + } else { + m = this.API.methods[methods[0]][methods[1]] + } + + let url = ''; + let mode = 'GET'; + if (typeof (m) == 'string') { + url = m; + } else { + url = m.url; + mode = m.mode || 'GET'; + } + + url = this.serverURL + this.replaceKeys(url, data); + + const options = { + method: mode, + headers: { + accept: 'application/json', + 'X-Emby-Token': `${this.options.apiKey}` + } + }; + + + try { + const response = await fetch(url, options); + if (response.ok) { + const result = await response.json(); + return result; + } else { + throw new Error(`HTTP 错误!状态码:${response.status}`); + } + } catch (error) { + + } + + return false; + } + + /** + * 验证服务器可用性 + */ + public async ping() { + const result = await this.execAPI('getSystemInfo'); + if (result && result.Id) { + return true; + } + + return false; + } + + /** + * 根据imdbId 获取媒体信息 + * @param imdbId + * @returns + */ + public async getMediaFromMediaServer(imdbId: string) { + const result = await this.execAPI('findFromIMDb', { + imdbId + }); + if (result && result.Items) { + return result; + } + + return false; + } +} \ No newline at end of file diff --git a/src/interface/common.ts b/src/interface/common.ts index abe4bed33..fcdb2901f 100644 --- a/src/interface/common.ts +++ b/src/interface/common.ts @@ -13,7 +13,8 @@ import { EWorkingStatus, EEncryptMode, ETorrentStatus, - ERequestType + ERequestType, + EMediaServerType } from "./enum"; /** @@ -27,7 +28,7 @@ export interface ContextMenuRules { export interface DownloadClient { id?: string; - enabled?:boolean; + enabled?: boolean; name?: string; // oldName?: string; address?: string; @@ -55,6 +56,16 @@ export interface QbCategory { path: string; } +export interface IMediaServer { + id: string; + enabled: boolean; + name: string; + address: string; + type: EMediaServerType; + apiKey?: string; + desc?: string; +} + /** * 助手按钮 */ @@ -119,6 +130,7 @@ export interface Options { exceedSizeUnit?: ESizeUnit; sites: any[]; clients: any[]; + mediaServers?: IMediaServer[]; pluginIconShowPages?: string[]; contextMenuRules?: ContextMenuRules; allowSelectionTextSearch?: boolean; @@ -384,7 +396,7 @@ export interface Request { data?: any; } -export interface IRequest extends Request {} +export interface IRequest extends Request { } export interface NoticeOptions { msg?: string; diff --git a/src/interface/enum.ts b/src/interface/enum.ts index 21c25444e..3d2d64eaf 100644 --- a/src/interface/enum.ts +++ b/src/interface/enum.ts @@ -256,7 +256,12 @@ export enum EAction { pushDebugMsg = "pushDebugMsg", updateDebuggerTabId = "updateDebuggerTabId", // 获取热门搜索 - getTopSearches = "getTopSearches" + getTopSearches = "getTopSearches", + + // 测试媒体服务器是否可连接 + testMediaServerConnectivity = "testMediaServerConnectivity", + // 从媒体服务器中获取信息 + getMediaFromMediaServer = "getMediaFromMediaServer" } /** @@ -388,6 +393,13 @@ export enum EBackupServerType { WebDAV = "WebDAV" } +/** + * 媒体服务器类型 + */ +export enum EMediaServerType { + Emby = "Emby" +} + /** * 插件显示位置 */ diff --git a/src/options/components/MediaServerInfoCard.vue b/src/options/components/MediaServerInfoCard.vue new file mode 100644 index 000000000..49e5063f8 --- /dev/null +++ b/src/options/components/MediaServerInfoCard.vue @@ -0,0 +1,234 @@ + + + + diff --git a/src/options/components/MovieInfoCard.vue b/src/options/components/MovieInfoCard.vue index 5d82017d8..9b916bcc4 100644 --- a/src/options/components/MovieInfoCard.vue +++ b/src/options/components/MovieInfoCard.vue @@ -222,6 +222,12 @@ + + @@ -230,10 +236,14 @@ import Vue from "vue"; import Extension from "@/service/extension"; import { EAction } from "@/interface/enum"; +import MediaServerInfoCard from "./MediaServerInfoCard.vue"; const extension = new Extension(); export default Vue.extend({ + components: { + MediaServerInfoCard + }, props: { IMDbId: String, doubanId: String @@ -360,6 +370,12 @@ export default Vue.extend({ return result.join(splitChar); } return ""; + }, + /** + * 从已定义的媒体服务器获取信息 + */ + getMediaFromMediaServers() { + } }, computed: { diff --git a/src/options/components/Navigation.vue b/src/options/components/Navigation.vue index 2deb5b9a1..53af5b878 100644 --- a/src/options/components/Navigation.vue +++ b/src/options/components/Navigation.vue @@ -6,14 +6,8 @@ $t(group.title) }}