diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml deleted file mode 100644 index 4ae3c084..00000000 --- a/.github/workflows/node.js.yml +++ /dev/null @@ -1,28 +0,0 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - -name: Node.js CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - - run: cd ./soda_resource_tools_webui && pwd && yarn install - - run: cd ./soda_resource_tools_webui && pwd && yarn run build --if-present -# - run: cd ./soda_resource_tools_webui && pwd && yarn test diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 31000a27..403f8df6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,7 +16,5 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Build - run: cargo build --verbose - name: Run tests - run: cargo test --verbose + run: cd ./soda_resource_tools_lib && cargo build && cargo test -- --test-threads=1 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..df406de8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,122 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'soda_cli'", + "cargo": { + "args": [ + "build", + "--bin=soda_cli", + "--package=soda_cli" + ], + "filter": { + "name": "soda_cli", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'soda_cli'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=soda_cli", + "--package=soda_cli" + ], + "filter": { + "name": "soda_cli", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'soda_resource_tools_lib'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=soda_resource_tools_lib" + ], + "filter": { + "name": "soda_resource_tools_lib", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'config_test'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=config_test", + "--package=soda_resource_tools_lib" + ], + "filter": { + "name": "config_test", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'integration_1_soda_test'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=integration_1_soda_test", + "--package=soda_resource_tools_lib" + ], + "filter": { + "name": "integration_1_soda_test", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'integration_2_meta_test'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=integration_2_meta_test", + "--package=soda_resource_tools_lib" + ], + "filter": { + "name": "integration_2_meta_test", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 8767ec8d..0d50c4f8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,79 @@ { - "window.zoomLevel": 2, + "cSpell.words": [ + "appender", + "Ascention", + "biezhihua", + "bitrates", + "bluray", + "characterart", + "chrono", + "clearart", + "dateadded", + "douban", + "episodedetails", + "Evangelion", + "fanart", + "filebrowser", + "filetransfer", + "FLAC", + "geilijiasu", + "hardlink", + "hdclearart", + "hdmovieclearart", + "hdmovielogo", + "hdtvlogo", + "HHWEB", + "Hstud", + "imdb", + "imdbid", + "lolice", + "Matroska", + "minidom", + "MNHD", + "moviebackground", + "moviebanner", + "moviedisc", + "movieposter", + "moviethumb", + "originaltitle", + "Rati", + "releasedate", + "remultiplexing", + "REMUX", + "remuxed", + "remuxing", + "reqwest", + "rmvb", + "seasonnumber", + "seasonposter", + "showbackground", + "themoviedb", + "thetvdb", + "tmdb", + "TMDBID", + "tvbanner", + "tvdb", + "tvdbid", + "tvposter", + "tvshow", + "tvthumb", + "uniqueid", + "Wolowitz", + "Xvid" + ], + "window.zoomLevel": 1, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, "terminal.integrated.shellArgs.windows": [ "/K", "chcp 65001" - ] -} \ No newline at end of file + ], + "[xml]": { + "editor.defaultFormatter": "redhat.vscode-xml" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + } +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..f7312679 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +resolver = "1" +members = [ + "soda_resource_tools_cli", + "soda_resource_tools_lib", +] diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 00000000..d7582e64 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,19 @@ +# https://juejin.cn/post/7325645956254351360 +# https://github.com/cross-rs/cross-toolchains +# https://hub.docker.com/r/freeznet/x86_64-apple-darwin-cross +# https://github.com/cross-rs/cross?tab=readme-ov-file + +# linux +[target.x86_64-unknown-linux-gnu] +xargo = false +# dockerfile = "D:\\Projects\\github\\cross\\docker\\Dockerfile.x86_64-unknown-linux-gnu" + +# windwos +[target.x86_64-pc-windows-gnu] +xargo = false +# dockerfile = "D:\\Projects\\github\\cross\\docker\\Dockerfile.x86_64-pc-windows-gnu" + +# macos +[target.x86_64-apple-darwin] +xargo = false +image = "freeznet/x86_64-apple-darwin-cross:11.3.1" \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9e..00000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index d36f64c2..1fd6897a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ -# Soda资源管理工具 +# soda -专为国内各类资源(影视(电影、剧集(电视剧、动漫、综艺、纪录片))、音乐(歌手、有声书)、书籍(书、写真、漫画))爱好者提供的集下载源管理(公域站点、私域站点、网盘)、内容订阅、资源下载、资源管理、媒体刮削、事件通知功能于一身的易上手的资源管理与自动化工具。 \ No newline at end of file +## local release + +```shell +cross build --target x86_64-apple-darwin --release +cross build --target x86_64-pc-windows-gnu --release +cross build --target x86_64-unknown-linux-gnu --release +``` \ No newline at end of file diff --git a/soda_resource_tools_cli/.gitignore b/soda_resource_tools_cli/.gitignore new file mode 100644 index 00000000..358f93d7 --- /dev/null +++ b/soda_resource_tools_cli/.gitignore @@ -0,0 +1,3 @@ +cache +target +log \ No newline at end of file diff --git a/soda_resource_tools_cli/.rustfmt.toml b/soda_resource_tools_cli/.rustfmt.toml new file mode 100644 index 00000000..a04e04b6 --- /dev/null +++ b/soda_resource_tools_cli/.rustfmt.toml @@ -0,0 +1,2 @@ +max_width = 150 +disable_all_formatting = false diff --git a/soda_resource_tools_cli/.vscode/settings.json b/soda_resource_tools_cli/.vscode/settings.json new file mode 100644 index 00000000..873715bf --- /dev/null +++ b/soda_resource_tools_cli/.vscode/settings.json @@ -0,0 +1,80 @@ +{ + "cSpell.words": [ + "appender", + "Ascention", + "biezhihua", + "bitrates", + "bluray", + "characterart", + "chrono", + "clearart", + "dateadded", + "douban", + "episodedetails", + "Evangelion", + "fanart", + "filebrowser", + "filetransfer", + "FLAC", + "geilijiasu", + "hardlink", + "hdclearart", + "hdmovieclearart", + "hdmovielogo", + "hdtvlogo", + "HHWEB", + "Hstud", + "imdb", + "imdbid", + "lolice", + "Matroska", + "minidom", + "MNHD", + "moviebackground", + "moviebanner", + "moviedisc", + "movieposter", + "moviethumb", + "originaltitle", + "Rati", + "releasedate", + "remultiplexing", + "REMUX", + "remuxed", + "remuxing", + "reqwest", + "rmvb", + "seasonnumber", + "seasonposter", + "showbackground", + "themoviedb", + "thetvdb", + "tmdb", + "TMDBID", + "tvbanner", + "tvdb", + "tvdbid", + "tvposter", + "tvshow", + "tvthumb", + "uniqueid", + "Wolowitz", + "Xvid" + ], + "window.zoomLevel": 1, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + "terminal.integrated.shellArgs.windows": [ + "/K", + "chcp 65001" + ], + "[xml]": { + "editor.defaultFormatter": "redhat.vscode-xml" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + } + } + \ No newline at end of file diff --git a/soda_resource_tools_cli/Cargo.toml b/soda_resource_tools_cli/Cargo.toml new file mode 100644 index 00000000..e5bbe99b --- /dev/null +++ b/soda_resource_tools_cli/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "soda_cli" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +keywords = ["tv or movie scrape", "scrape", "file recognize"] +homepage = "https://github.com/biezhihua/soda_cli" +documentation = "https://github.com/biezhihua/soda_cli/blob/main/README.md" +readme = "https://github.com/biezhihua/soda_cli/blob/main/README.md" + +[dependencies] +soda_resource_tools_lib = { path = "../soda_resource_tools_lib" } + +# 日志 +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", default-features = false, features = [ + "fmt", + "ansi", + "env-filter", + "tracing-log", + "registry", + "json", + "chrono", +] } +tracing-appender = "0.2.3" +clap = { version = "4.4.18", features = ["derive"] } +directories = "5.0.1" +reqwest = { version = "0.11.23", features = ["blocking", "json"] } +serde_json = "1.0.111" +serde = { version = "1.0.195", features = ["derive"] } +chrono = "0.4.32" diff --git a/soda_resource_tools_cli/README.md b/soda_resource_tools_cli/README.md new file mode 100644 index 00000000..3d7dd946 --- /dev/null +++ b/soda_resource_tools_cli/README.md @@ -0,0 +1,97 @@ +# CLI + +```shell +cargo run --bin soda_resource_tools_cl -- scrape --resource-type mt --transfer-type hard_link --src-dir D:\Downloads\Src --target-dir D:\Downloads\Test +``` + +```shell +cargo run --bin soda_cli -- --log-path D:\Projects\github\soda-resource-tools\soda_resource_tools_cli\log --cache-path D:\Projects\github\soda-resource-tools\soda_resource_tools_lib\cache scrape --resource-type mt --transfer-type symbol_link --src-dir \\NAS-TANK\downloads_disk5\电视剧\Agatha.Christies.Poirot.S01-S13.COMPLETE.1080p.BluRay.REMUX.AVC.DTS-HD.MA_DD_FLAC.2.0_5.1-EPSiLON\Agatha.Christies.Poirot.S01.1080p.BluRay.REMUX.AVC.DD.2.0-EPSiLON --target-dir D:\Downloads\Target +``` + +```shell +cargo run --bin soda_cli -- --log-path D:\Projects\github\soda-resource-tools\soda_resource_tools_cli\log --cache-path D:\Projects\github\soda-resource-tools\soda_resource_tools_lib\cache scrape --resource-type mt --transfer-type symbol_link --src-dir \\NAS-TANK\downloads_disk5\电视剧 --target-dir D:\Downloads\Target +``` + +```shell +cargo run --bin soda_cli -- --log-path D:\Projects\github\soda-resource-tools\soda_resource_tools_cli\log --cache-path D:\Projects\github\soda-resource-tools\soda_resource_tools_lib\cache scrape --resource-type mt --transfer-type symbol_link --src-dir \\NAS-TANK\downloads_disk1\动漫 --target-dir D:\Downloads\Target +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --resource-type mt --transfer-type symbol_link --src-dir \\NAS-TANK\downloads_disk5\电视剧 --target-dir D:\Downloads\Target +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --resource-type mt --transfer-type symbol_link --src-dir \\NAS-TANK\downloads_disk1\动漫 --target-dir D:\Downloads\Target +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir D:\Downloads\Target --src-dir \\NAS-TANK\downloads_disk3\纪录片 +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk3/纪录片/宇宙 1-9 季[全集]The.Universe.S01-S09.2007-2015.BD-REMUX.1080p.H264.AVC.AC3-FrankB" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk3\纪录片\Forged.in.Fire.S08.720p.WEBRip.AAC2.0.x264-MIXED[rartv]" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk3\纪录片\Great.British.Railway.Journeys.S01-S13.720p.HDTV.x264-Mixed" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk3\纪录片\Air crash investigation(空中浩劫)全" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk3\纪录片\A.Perfect.Planet.S01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk3\纪录片\City.of.Angels.City.of.Death.S01.2021.Disney+.WEB-DL.1080p.H264.DDP-HDCTV" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk1\动漫" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image true --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk1\动漫" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image false --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk8\电影" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image true --resource-type mt --transfer-type symbol_link --target-dir "D:\Downloads\Target" --src-dir "\\NAS-TANK\downloads_disk3\纪录片" +``` + +```shell +cargo run --bin soda_cli -- --dev true scrape --scrape-image true --resource-type mt --rename-style emby --transfer-type symbol_link --target-dir "D:\Downloads\Target\电影" --src-dir "\\NAS-TANK\downloads_disk8\电影\Spider-Man.Across.the.Spider-Verse.2023.2160p.MA.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX.mkv" +``` + +```shell + cargo run --bin soda_cli -- --dev true scrape --scrape-image true --resource-type mt --rename-style emby --transfer-type symbol_link --target-dir "D:\Downloads\Target\电影" --src-dir "\\NAS-TANK\downloads_disk8\电影\DouBan.2022.11.11.Top.250.BluRay.1080p.x265.10bit.MNHD-FRDS\东邪西毒终极版.Ashes.of.Time.Redux.2008.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS\Ashes.of.Time.Redux.2008.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv" +``` + +``` +A resource manage CLI + +Usage: soda.exe [OPTIONS] + +Commands: + scrape scrape resource + help Print this message or the help of the given subcommand(s) + +Options: + --cache-path + -h, --help Print help +``` + +``` +cargo run --bin soda -- --cache-path D:\Projects\github\soda-resource-tools\soda_resource_tools_lib\cache scrape --resource-type mt --transfer-type hard_link --src-dir D:\Downloads\Src + + cargo run --bin soda_cli -- --cache-path D:\Projects\github\soda-resource-tools\soda_resource_tools_lib\cache scrape --resource-type mt --transfer-type hard_link --src-dir D:\Downloads\Src +``` \ No newline at end of file diff --git a/soda_resource_tools_cli/src/main.rs b/soda_resource_tools_cli/src/main.rs new file mode 100644 index 00000000..b49055d0 --- /dev/null +++ b/soda_resource_tools_cli/src/main.rs @@ -0,0 +1,385 @@ +use std::{ + env::current_dir, + fs, + path::{Path, PathBuf}, +}; + +use directories::ProjectDirs; +use serde_json::Value; +use soda_resource_tools_lib::soda::{ + self, + entity::{RenameStyle, ResourceType, ScrapeConfig, SodaError, TransferType}, +}; +use tracing_appender::non_blocking::NonBlocking; +use tracing_subscriber::{filter, fmt::time::ChronoLocal, layer::SubscriberExt, util::SubscriberInitExt, Layer}; + +use clap::builder::TypedValueParser as _; +use clap::Parser; +use clap::Subcommand; + +#[derive(Debug, Parser)] +#[command(name = "soda")] +#[command(about = "A resource scrape CLI", long_about = None)] +struct Cli { + /// 开发模式 + #[arg(long)] + dev: Option, + + /// 日志路径 + #[arg(long,value_hint = clap::ValueHint::DirPath)] + log_path: Option, + + /// 日志级别 + #[arg(long, default_value = "debug", value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]))] + log_level: Option, + + /// 缓存路径 + #[arg(long,value_hint = clap::ValueHint::DirPath)] + cache_path: Option, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + /// 刮削资源 + #[command(arg_required_else_help = true)] + Scrape { + /// 媒体类型 + /// mt: 电影和电视剧 + #[arg( + long, + default_value_t = ResourceType::MT, + value_parser = clap::builder::PossibleValuesParser::new(["mt"]) + .map(|s| s.parse::().unwrap()), + )] + resource_type: ResourceType, + + /// 媒体从源目录转移到输出目录的方式 + /// hard_link: 硬链接 + /// symbol_link: 符号链接 + /// copy: 复制 + /// move: 移动 + #[arg( + long, + default_value_t = TransferType::HardLink, + value_parser = clap::builder::PossibleValuesParser::new(["hard_link", "symbol_link", "copy", "move"]) + .map(|s| s.parse::().unwrap()), + )] + transfer_type: TransferType, + + /// 刮削图片 + /// true: 刮削图片 + /// false: 不刮削图片 + #[arg(long, default_value_t = true)] + scrape_image: bool, + + /// 重命名格式 + /// emby: Emby格式 + #[arg( long, default_value_t = RenameStyle::Emby, value_parser = clap::builder::PossibleValuesParser::new(["emby"]) + .map(|s| s.parse::().unwrap()), + )] + rename_style: RenameStyle, + + /// 媒体源目录 + #[arg(long,value_hint = clap::ValueHint::DirPath)] + src_dir: Option, + + /// 媒体刮削输出目录 + /// 刮削后的文件输出目录,如果不指定则默认为src_dir + #[arg(long,value_hint = clap::ValueHint::DirPath)] + target_dir: Option, + }, +} + +fn main() -> Result<(), SodaError> { + // 解析命令行参数 + let args = Cli::parse(); + + // 开发模式 + let dev = args.dev.unwrap_or(false); + + // 获取配置文件目录 + let proj_dirs = ProjectDirs::from("com", "biezhihua", "soda").unwrap(); + + // 创建配置文件目录 + let config_dir = proj_dirs.config_dir(); + fs::create_dir_all(config_dir)?; + + // 创建缓存文件目录 + let mut cache_dir = args.cache_path.unwrap_or(proj_dirs.cache_dir().join("cache")); + if dev { + cache_dir = current_dir()?.parent().unwrap().join("soda_resource_tools_lib").join("cache"); + } + let cache_dir = Path::new(&cache_dir); + fs::create_dir_all(cache_dir)?; + + // 创建日志文件目录 + let log_dir = if dev { + cache_dir.join("log") + } else { + args.log_path.unwrap_or(proj_dirs.cache_dir().join("log")) + }; + let log_dir = Path::new(&log_dir); + clean_dir(&log_dir.to_path_buf()); + fs::create_dir_all(log_dir)?; + + // 配置日志 + let log_level = args.log_level.unwrap_or("info".to_string()); + + // 配置日志 + let all_file_appender = tracing_appender::rolling::never(log_dir, "all.log"); + let (all_log, _guard) = tracing_appender::non_blocking(all_file_appender); + + // 配置日志 + let metadata_file_appender = tracing_appender::rolling::never(log_dir, "metadata.log"); + let (metadata_log, _guard) = tracing_appender::non_blocking(metadata_file_appender); + + // 日志初始化 + init_tracing(log_level, all_log, metadata_log); + + tracing::info!(target:"soda::info", "配置文件目录: {}", config_dir.to_str().unwrap()); + tracing::info!(target:"soda::info", "缓存文件目录: {}", cache_dir.to_str().unwrap()); + tracing::info!(target:"soda::info", "日志文件目录: {}", log_dir.to_str().unwrap()); + + // 检查网络 + check_internet()?; + + match args.command { + Commands::Scrape { + resource_type, + transfer_type, + src_dir, + target_dir, + scrape_image, + rename_style, + } => { + // 开发者配置 + if dev { + let lib_dir = current_dir()?.parent().unwrap().join("soda_resource_tools_lib"); + init_lib_config_dev(&lib_dir, &rename_style); + } + // Release配置 + else { + if let Ok(()) = init_config(&config_dir) { + tracing::info!(target:"soda::info", "初始化配置文件成功"); + } else { + tracing::error!(target:"soda::info", "初始化配置文件失败,请检查网络后重试"); + return Err(SodaError::Str("初始化配置文件失败,请检查网络后重试")); + } + + let local_soda_config_path = config_dir.join("soda_config.json"); + let local_soda_config: Value = serde_json::from_str(&fs::read_to_string(&local_soda_config_path)?)?; + tracing::info!(target:"soda::info", "配置文件: {}", local_soda_config); + + if !local_soda_config.get("enable_cli").unwrap().as_bool().unwrap() { + tracing::error!(target:"soda::info", "配置文件中enable_cli为false,不允许使用soda_cli"); + return Ok(()); + } + + init_lib_config(&local_soda_config, &config_dir, &cache_dir, &rename_style); + } + + if src_dir.is_some() { + let src_dir = src_dir.clone().unwrap(); + let target_dir = if target_dir.is_some() { target_dir.unwrap() } else { src_dir.clone() }; + if src_dir.exists() { + if !target_dir.exists() { + fs::create_dir_all(target_dir.clone()).unwrap(); + tracing::info!(target:"soda::info", "创建媒体刮削输出目录: {}", target_dir.to_str().unwrap()); + } + let src_dir = src_dir.to_str().unwrap().to_string(); + let target_dir = target_dir.to_str().unwrap().to_string(); + scrape_mt(resource_type, transfer_type, src_dir, target_dir, scrape_image); + } else { + tracing::error!(target:"soda::info", "媒体源目录不存在") + } + } + } + } + + return Ok(()); +} + +fn check_internet() -> Result<(), SodaError> { + tracing::info!(target:"soda::info", "开始检查网络"); + + tracing::info!(target:"soda::info", "开始访问: https://raw.githubusercontent.com/biezhihua/soda_cli/main/soda_config.json" ); + let _ = reqwest::blocking::get("https://raw.githubusercontent.com/biezhihua/soda_cli/main/soda_config.json")?; + + tracing::info!(target:"soda::info", "开始访问: https://api.themoviedb.org" ); + // let _ = reqwest::blocking::Client::new().post("https://api.themoviedb.org").send()?; + let _ = reqwest::blocking::get("https://api.themoviedb.org")?; + + tracing::info!(target:"soda::info", "开始访问: https://webservice.fanart.tv" ); + // let _ = reqwest::blocking::Client::new().post("https://webservice.fanart.tv").send()?; + let _ = reqwest::blocking::get("https://webservice.fanart.tv")?; + + return Ok(()); +} + +fn init_lib_config(local_soda_config: &Value, config_dir: &Path, cache_dir: &Path, rename_style: &RenameStyle) { + let bin_path_name = local_soda_config.get("bin").unwrap().as_str().unwrap(); + let soda_config_bin_path = config_dir.join(bin_path_name); + let soda_config_bin_content = fs::read_to_string(&soda_config_bin_path).unwrap(); + let soda_config_bin_json: Value = serde_json::from_str(&soda_config_bin_content).unwrap(); + + let mt_strong_match_rules_tv = soda_config_bin_json.get("mt_strong_match_rules_tv").unwrap(); + let mt_strong_match_rules_movie = soda_config_bin_json.get("mt_strong_match_rules_movie").unwrap(); + let mt_strong_match_regex_rules = soda_config_bin_json.get("mt_strong_match_regex_rules").unwrap(); + let mt_strong_match_name_map = soda_config_bin_json.get("mt_strong_match_name_map").unwrap(); + + let mut config = soda::get_lib_config(); + config.cache_path = cache_dir.to_str().unwrap().to_string(); + config.strong_match_name_map = serde_json::to_string(mt_strong_match_name_map).unwrap(); + config.strong_match_regex_rules = serde_json::to_string(mt_strong_match_regex_rules).unwrap(); + config.strong_match_rules_tv = serde_json::to_string(mt_strong_match_rules_tv).unwrap(); + config.strong_match_rules_tv_path = "".to_string(); + config.strong_match_rules_movie = serde_json::to_string(mt_strong_match_rules_movie).unwrap(); + config.strong_match_rules_movie_path = "".to_string(); + config.strong_match_regex_rules_path = "".to_string(); + config.strong_match_name_map_path = "".to_string(); + config.metadata_skip_special = true; + config.rename_style = Some(rename_style.clone()); + soda::update_lib_config(config); + tracing::info!(target:"soda::info", "配置更新成功"); +} + +fn init_lib_config_dev(lib_dir: &Path, rename_style: &RenameStyle) { + let mut config = soda::get_lib_config(); + config.cache_path = lib_dir.join("cache").to_str().unwrap().to_string(); + config.strong_match_rules_tv_path = lib_dir.join("config").join("mt_strong_match_rules_tv.json").to_str().unwrap().to_string(); + config.strong_match_rules_movie_path = lib_dir + .join("config") + .join("mt_strong_match_rules_movie.json") + .to_str() + .unwrap() + .to_string(); + config.strong_match_regex_rules_path = lib_dir + .join("config") + .join("mt_strong_match_regex_rules.json") + .to_str() + .unwrap() + .to_string(); + config.strong_match_name_map_path = lib_dir.join("config").join("mt_strong_match_name_map.json").to_str().unwrap().to_string(); + config.rename_style = Some(rename_style.clone()); + config.metadata_skip_special = true; + soda::update_lib_config(config); + tracing::info!(target:"soda::info", "配置更新成功"); +} + +fn init_config(config_dir: &Path) -> Result<(), SodaError> { + tracing::info!(target:"soda::info", "开始从Github获取配置文件"); + + let soda_config_url = format!("{}/soda_config.json", raw_github()); + + let remote_soda_config: Value = reqwest::blocking::get(soda_config_url)?.json()?; + + tracing::info!(target:"soda::info", "获取配置文件成功: {}", remote_soda_config); + + let local_soda_config_path = config_dir.join("soda_config.json"); + if !local_soda_config_path.exists() { + update_soda_config(&local_soda_config_path, &remote_soda_config, config_dir)?; + } else { + let local_soda_config: Value = serde_json::from_str(&fs::read_to_string(&local_soda_config_path)?)?; + let local_version = local_soda_config.get("version").unwrap().as_i64().unwrap(); + let remote_version = remote_soda_config.get("version").unwrap().as_i64().unwrap(); + if local_version < remote_version { + update_soda_config(&local_soda_config_path, &remote_soda_config, config_dir)?; + } + } + return Ok(()); +} + +fn raw_github() -> &'static str { + return "https://raw.githubusercontent.com/biezhihua/soda_cli/main"; +} + +fn api_themoviedb() -> &'static str { + return "https://api.themoviedb.org"; +} + +fn api_fanart() -> &'static str { + return "https://webservice.fanart.tv"; +} + +fn update_soda_config( + local_soda_config_path: &std::path::PathBuf, + remote_soda_config: &Value, + config_dir: &std::path::Path, +) -> Result<(), SodaError> { + // write soda_config.json + fs::write(local_soda_config_path, remote_soda_config.to_string())?; + + let bin = remote_soda_config + .get("bin") + .ok_or(SodaError::Str("bin字段不存在"))? + .as_str() + .ok_or(SodaError::Str("bin字段不是字符串"))?; + + let bin_url = format!("{}/{}", raw_github(), bin); + + tracing::info!(target:"soda::info", "开始从Github获取bin文件: {}", bin_url); + + let mut local_bin_file = fs::File::create(config_dir.join(bin))?; + + reqwest::blocking::get(bin_url)?.copy_to(&mut local_bin_file)?; + + return Ok(()); +} + +fn init_tracing(log_level: String, all_log: NonBlocking, metadata_log: NonBlocking) { + let filter = match log_level.as_str() { + "trace" => filter::LevelFilter::TRACE, + "debug" => filter::LevelFilter::DEBUG, + "info" => filter::LevelFilter::INFO, + "warn" => filter::LevelFilter::WARN, + "error" => filter::LevelFilter::ERROR, + _ => filter::LevelFilter::INFO, + }; + tracing_subscriber::registry() + .with( + // 收集INFO级别以上的soda_cli日志输出到控制台 + tracing_subscriber::fmt::layer() + .with_timer(ChronoLocal::new("%Y-%m-%d %H:%M:%S".to_string())) + .with_filter(filter::LevelFilter::INFO) + .with_filter(filter::filter_fn(|metadata| metadata.target().starts_with("soda::info"))), + ) + .with( + // 收集DEBUG级别以上的日志到debug.log文件 + tracing_subscriber::fmt::layer() + .with_timer(ChronoLocal::new("%Y-%m-%d %H:%M:%S".to_string())) + .with_writer(all_log) + .with_ansi(false) + .with_filter(filter), + ) + .with( + // 收集INFO级别以上的日志到metadata_log文件 + tracing_subscriber::fmt::layer() + .with_timer(ChronoLocal::new("%Y-%m-%d %H:%M:%S".to_string())) + .with_writer(metadata_log) + .with_ansi(false) + .with_filter(filter::LevelFilter::INFO) + .with_filter(filter::filter_fn(|metadata| metadata.target().starts_with("soda::metadata"))), + ) + .init(); +} + +fn scrape_mt(resource_type: ResourceType, transfer_type: TransferType, src_dir: String, target_dir: String, scrape_image: bool) { + tracing::info!(target:"soda::info", "刮削开始: 媒体类型: {:?}, 媒体从源目录转移到输出目录的方式: {:?}, 媒体源目录: {:?}, 媒体刮削输出目录: {:?}",resource_type, transfer_type, src_dir, target_dir); + + let mut scrape_config = ScrapeConfig::new(); + scrape_config.enable_scrape_image = scrape_image; + scrape_config.enable_recognize = true; + + soda::scrape(resource_type, transfer_type, scrape_config, src_dir, target_dir); + + tracing::info!(target:"soda::info", "刮削结束"); +} + +fn clean_dir(dir: &PathBuf) { + if dir.exists() { + std::fs::remove_dir_all(dir).unwrap(); + } +} diff --git a/soda_resource_tools_lib/.rustfmt.toml b/soda_resource_tools_lib/.rustfmt.toml index 988a7b7c..97f4d916 100644 --- a/soda_resource_tools_lib/.rustfmt.toml +++ b/soda_resource_tools_lib/.rustfmt.toml @@ -1,2 +1,2 @@ -max_width=500 +max_width=150 disable_all_formatting = false \ No newline at end of file diff --git a/soda_resource_tools_lib/.vscode/launch.json b/soda_resource_tools_lib/.vscode/launch.json new file mode 100644 index 00000000..0581790e --- /dev/null +++ b/soda_resource_tools_lib/.vscode/launch.json @@ -0,0 +1,84 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'soda_resource_tools_lib'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=soda_resource_tools_lib" + ], + "filter": { + "name": "soda_resource_tools_lib", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'config_test'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=config_test", + "--package=soda_resource_tools_lib" + ], + "filter": { + "name": "config_test", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'integration_meta_test'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=integration_meta_test", + "--package=soda_resource_tools_lib" + ], + "filter": { + "name": "integration_meta_test", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'integration_soda_test'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=integration_soda_test", + "--package=soda_resource_tools_lib" + ], + "filter": { + "name": "integration_soda_test", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/soda_resource_tools_lib/.vscode/settings.json b/soda_resource_tools_lib/.vscode/settings.json index 602cd9c0..0d50c4f8 100644 --- a/soda_resource_tools_lib/.vscode/settings.json +++ b/soda_resource_tools_lib/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "appender", "Ascention", + "biezhihua", "bitrates", "bluray", "characterart", @@ -18,6 +19,8 @@ "geilijiasu", "hardlink", "hdclearart", + "hdmovieclearart", + "hdmovielogo", "hdtvlogo", "HHWEB", "Hstud", @@ -27,6 +30,11 @@ "Matroska", "minidom", "MNHD", + "moviebackground", + "moviebanner", + "moviedisc", + "movieposter", + "moviethumb", "originaltitle", "Rati", "releasedate", diff --git a/soda_resource_tools_lib/Cargo.toml b/soda_resource_tools_lib/Cargo.toml index ed12299d..c8743d4e 100644 --- a/soda_resource_tools_lib/Cargo.toml +++ b/soda_resource_tools_lib/Cargo.toml @@ -12,19 +12,14 @@ path = "src/lib.rs" [dependencies] # 正则 -regex = "1.10.2" +regex = "1.10.3" # JSON -serde_json = "1.0.108" -serde = { version = "1.0.193", features = ["derive"] } - -# 日志 -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", default-features = false, features = ["fmt", "ansi", "env-filter", "tracing-log"] } -tracing-appender = "0.2.3" +serde_json = "1.0.111" +serde = { version = "1.0.195", features = ["derive"] } # 加密 -ring = "0.17.5" +ring = "0.17.7" data-encoding = "2.5.0" rand = "0.8.5" jwt = "0.16.0" @@ -32,19 +27,19 @@ hmac = "0.12.1" sha2 = "0.10.8" # 时间 -chrono = "0.4.31" +chrono = "0.4.32" # 文件监控 notify = { version = "6.1.1", features = ["serde"] } # 懒加载 -once_cell = { version = "1.18.0", features = [] } +once_cell = { version = "1.19.0", features = [] } # 网络 -reqwest = { version = "0.11.22", features = ["blocking", "json"] } +reqwest = { version = "0.11.23", features = ["blocking", "json"] } # 异步 -tokio = { version = "1.34.0", features = ["full"] } +tokio = { version = "1.35.1", features = ["full"] } # URL编码 urlencoding = "2.1.3" @@ -58,4 +53,15 @@ xml-rs = "0.8.19" # bytes bytes = "1.5.0" -[dev-dependencies] +magic-crypt = "3.1.13" + +tracing = "0.1.40" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.18", default-features = false, features = [ + "fmt", + "ansi", + "env-filter", + "tracing-log", + "registry", + "json" +] } diff --git a/soda_resource_tools_lib/README.md b/soda_resource_tools_lib/README.md index 776accde..bb15bf2a 100644 --- a/soda_resource_tools_lib/README.md +++ b/soda_resource_tools_lib/README.md @@ -91,8 +91,6 @@ X265 和 H.265 都是视频编码标准,通常也被称为 High Efficiency Vid 请注意,这些字段的含义可以根据上下文和特定的应用领域有所不同。如果你在特定的领域或项目中遇到这些字段,请查看相关文档或说明以获取更准确的含义。 -## 字段含义 - 以下是你提供的音频编解码和音频格式相关字段的含义解释: 1. "DTS-HD.MA.5.1.3Audio":这表示音频编码为DTS-HD Master Audio,包括5.1声道,并有3个不同的音频流。 @@ -178,6 +176,11 @@ X265 和 H.265 都是视频编码标准,通常也被称为 High Efficiency Vid 在媒体文件名 "Skam.S2E02.你对一个朋友撒谎却怪罪于我.SweSub.1080p.WEB-DL.H264.mp4" 中,“SweSub” 通常指的是 "Swedish Subtitles",意即“瑞典语字幕”。这表明该视频文件包含瑞典语的字幕选项。这种标记在不同语言版本的电视剧或电影中常见,用来指示附加的字幕语言。 + +## EE + +EE: This likely stands for "Extended Edition," which suggests that this version of the film includes additional footage not seen in the original theatrical release. + ## 包大小 File .text Size Crate @@ -202,4 +205,5 @@ X265 和 H.265 都是视频编码标准,通常也被称为 High Efficiency Vid 0.4% 1.4% 114.0KiB xml 0.3% 1.1% 85.7KiB url 3.3% 12.6% 1015.4KiB And 85 more crates. Use -n N to show more. -26.2% 100.0% 7.8MiB .text section size, the file size is 29.9MiB \ No newline at end of file +26.2% 100.0% 7.8MiB .text section size, the file size is 29.9MiB + diff --git a/soda_resource_tools_lib/config/mt_strong_match_name_map.json b/soda_resource_tools_lib/config/mt_strong_match_name_map.json new file mode 100644 index 00000000..2df39e6a --- /dev/null +++ b/soda_resource_tools_lib/config/mt_strong_match_name_map.json @@ -0,0 +1,172 @@ +{ + "names": [ + { + "src": { + "title_en": "The Ivory Tower", + "title_cn": "", + "release_year": "" + }, + "target": { + "title_en": "The Great White Tower", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "One Dream One Home", + "title_cn": "", + "release_year": "" + }, + "target": { + "title_en": "Macau Family", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "Remarriage and Desires", + "title_cn": "", + "release_year": "" + }, + "target": { + "title_en": "Remarriage & Desires", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "Shinbun Kisha", + "title_cn": "", + "release_year": "" + }, + "target": { + "title_en": "The Journalist", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "", + "title_cn": "24小时", + "release_year": "" + }, + "target": { + "title_en": "", + "title_cn": "24", + "release_year": "" + } + }, + { + "src": { + "title_en": "Bian Jiang Xing", + "title_cn": "", + "release_year": "" + }, + "target": { + "title_en": "CCTV Traveling Along the Border", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "Through The Wormhole With Morgan Freeman", + "title_cn": "", + "release_year": "" + }, + "target": { + "title_en": "Through the Wormhole", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "History of the World", + "title_cn": "", + "release_year": "2008" + }, + "target": { + "title_en": "", + "title_cn": "世界历史", + "release_year": "" + } + }, + { + "src": { + "title_en": "A One and a Two", + "title_cn": "", + "release_year": "2008" + }, + "target": { + "title_en": "Yi Yi", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "A Chinese Odyssey Part 1", + "title_cn": "", + "release_year": "1995" + }, + "target": { + "title_en": "A Chinese Odyssey Part One Pandoras Box", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "A Chinese Odyssey Part 2", + "title_cn": "", + "release_year": "1995" + }, + "target": { + "title_en": "A Chinese Odyssey Part Two Cinderella", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "Hachi A Dog'sTale", + "title_cn": "", + "release_year": "2009" + }, + "target": { + "title_en": "Hachi A Dog's Tale", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "The Trueman Show", + "title_cn": "", + "release_year": "2009" + }, + "target": { + "title_en": "The Truman Show", + "title_cn": "", + "release_year": "" + } + }, + { + "src": { + "title_en": "Leon", + "title_cn": "", + "release_year": "1994" + }, + "target": { + "title_en": "Léon: The Professional", + "title_cn": "", + "release_year": "" + } + } + ] +} \ No newline at end of file diff --git a/soda_resource_tools_lib/config/mt_strong_match_regex_rules.json b/soda_resource_tools_lib/config/mt_strong_match_regex_rules.json index f83622ba..64cd4eda 100644 --- a/soda_resource_tools_lib/config/mt_strong_match_regex_rules.json +++ b/soda_resource_tools_lib/config/mt_strong_match_regex_rules.json @@ -1,74 +1,212 @@ { - "source": [ - "(WEB|BD|BR|HD|DVD|HDTV)[Rr][Ii][Pp]", - "Blu[-]?[Rr]ay[ .]?(Remux|REMUX)?", - "(AVC|BD)?[.-]?(Remux|REMUX)", - "WEB(-DL)?", - "HDTV", - "CAM" + "edition": [ + "Extended\\.Collector's\\.Edition\\.Hybrid", + "Repack\\.H", + "Directors\\.Cut\\.REMUX", + "Directors\\.Cut", + "Director's\\.Cut", + "Extended\\.Cut", + "EXTENDED\\.ALTERNATE\\.CUT", + "internal\\.multi", + "EXTENDED\\.REMASTERED", + "25th\\.Anniversary\\.Remastered\\.Edition", + "Remastered\\.Edition", + "Remastered", + "Criterion\\.Collection\\.REMUX", + "NORDiC\\.REMUX", + "Extended", + "NORDiC", + "PROPER", + "REMASTERED", + "Open\\.Matte", + "Repack", + "sample", + "SAMPLE", + "Redux", + "Sample", + "UNCUT", + "HYBRID", + "Hybrid", + "MULTI", + "MULTi", + "SP", + "CC", + "EE" + ], + "version": [ + "V\\d{1}" + ], + "resolution_cn": [ + "标清", + "高清", + "超清" + ], + "episode_title_jp": [ + "[\\p{Hiragana}\\p{Katakana}\\p{Han}]+" + ], + "episode_title_cn": [ + "[\\p{Script=Han},]+", + "\\p{Script=Han}+[0-9]{1,4}\\p{Script=Han}+" + ], + "episode_title_en": [ + "[&-\\. A-Za-z]+[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}", + "[&-\\. A-Za-z]+[0-9]{1,4}[&-\\. A-Za-z]+", + "[&-\\. A-Za-z]+[0-9]{1,3}", + "[&-\\. A-Za-zñû]+|[0-9]+\\+[0-9]+", + "[0-9]{1,3}[&-\\. A-Za-z]+" + ], + "title_number_en": [ + "[!×\\()&'-\\. 0-9A-Za-z]+" + ], + "title_en": [ + "[∙Ⅱ~!×I&'-\\. A-Za-z]+" + ], + "title_number_cn": [ + "[0-9A-Za-z]*\\p{Script=Han}+[:0-9A-Za-z!!·]*\\p{Script=Han}*" + ], + "title_cn": [ + "\\p{Script=Han}+.\\p{Script=Han}+", + "\\p{Script=Han}+[A-Z]?" + ], + "season_title_cn": [ + "\\p{Script=Han}+" + ], + "subtitle_en": [ + "SweSub" + ], + "country": [ + "ESP", + "TW", + "KOREA", + "CHN", + "JPN", + "JP", + "Ger", + "GER", + "USA", + "US", + "KOR", + "HK" + ], + "resolution": [ + "3840[Xx]2160", + "1920[Xx]1080", + "240[Ppi]", + "360[Ppi]", + "480[Ppi]", + "576[Pp]", + "720[Ppi]", + "1080[Ppi]", + "1440[Ppi]", + "2160[Ppi]", + "4320[Ppi]", + "4[Kk]" ], "company": [ "壹高清", "Disney\\+", "myTV-SUPER", + "Crunchyroll", "Netflix", + "TVB Anywhere", + "B-Global", + "INTERNAL", + "BFI", + "Amazon", + "iTunes(\\.2Audio)?", + "UHDTV", + "ViuTV", "DSNP", "Hulu", "Jade", + "CCTV[1-9]?(-HD)?", + "ATVP", + "HULU", + "ARTE", + "KKTV", + "JSTV", + "HMAX", "AMZN", + "SCI", "AOD", - "CC", + "DBOX", + "FOD", + "BBC", + "TVB", + "DC", "HQ", "NF" ], + "source": [ + "(Web|web|WEB|BD|BR|HD|DVD|HDTV)[Rr][Ii][Pp]", + "[B|b]lu[-]?[Rr]ay[ \\.]?(Remux|ReMuX|REMUX)?", + "(AVC|BD)?[\\.-]?(Remux|REMUX)", + "(REMUX|UHD)[\\. ][B|b]lu[Rr]ay", + "(WEB|web|Web)([_ -]DL)?", + "HDTV", + "CAM" + ], "video_codec": [ "[XxHh]\\.?26[45]", "(HEVC|Hevc)", - "MPEG-\\d", - "VC-\\d", + "MPEG-?\\d", + "VC-?\\d", + "AVC\\.(Remux|REMUX)", "VP9", - "AV1", - "VC1", - "AVC" + "AV[1C]" ], "color": [ - "\\d{2}[Ff][Pp][Ss]", - "Hi10p", - "Ma10P", - "10[Bb]it.HDR", - "10[Bb]it", - "HDR([. ])?(DV)?", - "DV([. ])?(HDR)?", + "\\d{2,3}[Ff][Pp][Ss]", + "Hi10[Pp]", + "Ma10[Pp]", + "HLG10", + "HDR10\\+", + "\\(10bit\\)", + "10[Bb]it\\.HDR10\\+", + "10[Bb]it\\.HDR", + "10[Bb]it[s]?", + "HDR[\\. ]DV", + "DV[\\. ]HDR", + "DoVi", + "(HDR|hdr)", "UHD", + "HLG", "GBR", "SDR", - "HLG10", - "HDR10\\+" + "DV" ], "audio_codec": [ - "Atmos[ .]?TrueHD[.]?7.1", - "TrueHD[. ]?7.1", - "D[Tt][Ss]-HD([. ])?(M[Aa])?([. ])?(7.1|5.1|2.0)?([. ])?(\\dAudio[s]?)?", - "DTS.\\dAudio[s]?", - "DTS([. ])?(7.1|5.1|2.0)?", + "TrueHD\\.Atmos\\.7\\.1", + "Atmos[ \\.]?TrueHD[\\. ]?7\\.1", + "TrueHD[\\. ]?7\\.1", + "TrueHD\\.Atmos[ ]?7\\.1", + "TrueHD\\.5\\.", + "D[Tt][Ss]-HD([\\. ])?(HR|M[Aa])?([\\. _])?(7\\.1|5\\.1|2\\.0)?([\\. ])?(\\dAudio[s]?)?", + "DTS\\.\\dAudio[s]?", + "DTS([\\. ])?(7\\.1|5\\.1|2\\.0)?", + "DTS-HD MA 7 1", + "DTS-HD 3\\.0", + "DTSHD-MA", "DTS-X", - "DDP([. ])?(7.1|5.1|2.0)?([. ])?(Atmos)?(.&.)?(AAC)?", - "DD[.+]?(7.1|5.1|2.0)", - "\\d[Aa]udio", - "[Aa]tmos", - "FLAC.2.0", - "FLACx2", - "FLAC", - "EAC3", - "DDP", - "AC3", "D[Tt][Ss]", - "AAC", - "DD" + "DTSE5\\.1\\.\\dAudios?", + "DD[P]?[\\.\\+ ]?(7[\\. ]1|5[\\. ]1|2[\\. ]0)?[\\. ]?(Atmos|Atoms)?[\\. ]?(\\dAudio)?(\\.&\\.)?(AAC)?", + "\\d[Aa]udio[s]?", + "[Aa]tmos?", + "AAC\\(5\\.1\\)\\.\\dAudios?", + "AAC2\\.0", + "AAC(\\.?[\\d]?Audio[s]?)?", + "AC3\\.AAC", + "FLAC(x2|\\.2\\.0)?", + "flac(_aac)?", + "FLAC\\.H", + "E?[Aa][Cc]3" ], "release_group": [ "不着调字幕组", "织梦字幕组", + "KRaLiMaRKo", + "Nekomoe kissaten&VCB-Studio", "Billion Meta Lab & 7³ACG", "TrollUHD-ULTRAHDCLUB", "7³ACG x Sakurato", @@ -81,78 +219,190 @@ "ManGor@MTeam", "WGXC@HDFans", "DiY@HDHome", - "£cXcY@FRDS", - "MNHD-FRDS", - "mUHD-FRDS", - "cXcY@FRDS", - "Yumi@FRDS", + "(MNHDR|MNHD|mUHD|cXcY|Yumi)[-|@]FRDS", "Mandarin-OPS", "DVB@szsddqwx", "lolice-mix", + "VCB-Studio", + "philosophy-raws", + "decatora27", "LeagueWEB", + "BRiTiSHB00Bs", "FraMeSToR", + "mawen1250", + "MVGroup\\.org", + "th71@beAst", "TrollUHD", "QHstudIo", + "xiaofriend", + "HDPter", "EPSiLON", + "playWEB", + "FFans@ws林小凡", "Cinefeel", "WhaleHu", + "FuzerHD", "PTerWEB", + "decibeL", + "FOXKING", "SiCFoI", - "CHD(Bits|PAD|HKTV|WEB)", + "CHD(Bits|PAD|HKTV|WEB)?", "KYTiCE", - "HDH(ome|Pad|TV|WEB)", + "HDH(ome|Pad|TV|WEB)?", "AQLJ", + "HDChina", + "BORDURE", + "Absinth", + "OurBits", + "HaresWEB", + "CyTSuNee", "HDSWEB", - "HDS(ky|TV|Pad|WEB)", + "HDS(ky|TV|Pad|WEB)?", + "NC-Raws", + "AMRAP", "KAIZEN", "OneHD", + "SECTOR7-4P", + "MOOVEE", "NOGRP", "MZABI", "CNHK", "HDVWEB", + "SPADE", "HDCTV", "HHWEB", "LoliHouse", "Nekomoe kissaten", + "cXcY@FRDS", "HDFans", + "BluEvo", + "CNts", "SMURF", "OurTV", + "CasStudio", + "FLTTH", "7³ACG", + "FrankB", + "SharkWEB", "Stone", + "LEGi0N-Sample", "ADWeb", + "FraMeSToR-4P", + "HDMaNiAcS", + "CRiSC", + "CtrlHD", "Gamma", + "sergey_krs", + "carapils", + "AROMA", "SiGMA", + "WiKi\\.Sample", + "Japhson", + "iNTENSO", + "LEGi0N", "Dream", + "CiNEFiLE", + "cinefile", + "FoRM", + "LoRD", "PTsbao", + "GOSSIP", + "GREENOTEA", + "CAFFEiNE", + "KOMPOST", + "nikt0", + "SA89", + "4K4U", + "VLAD", + "BOOP", + "ROBOTS", + "DiVULGED", + "MNHD-FRDS", + "MnHD-FRDS", + "MNHD-FRD", + "Enichi", + "EXPLOIT", + "ESPRESSO", + "PECULATE", "PTer(DIY|Game|TV|WEB)", "PTH(Audio|eBook|music|ome|tv|WEB)", + "ETHiCS", + "LTF", "OPS", "SGXT", "PuTao", + "FOXKING", "Yumi", "PiR8", "APEX", "CMCT", "KOGi", + "R\\.KNOR", + "BOOP", + "NOGRP", + "ROBOTS", + "bigdoc", "JKCT", "EPiC", + "FGT", + "Chotab", + "PTHweb", "DBTV", + "BeiTai", + "beAst", + "TTVa", "CHD", "CiA", + "TWA", + "QOQ", + "ggwp", + "TEPES", + "iFT", + "GTi", + "NoGroup", + "iPRiP", + "XEBEC", + "KiNGS", + "AJP69", "PiR8", + "monkee", + "DON", + "FLUX", + "ZmWeb", + "ggwp", + "FRDS", + "glhf", + "HD4U", + "ggez", + "Slay3R", + "RARBG", "WiKi", + "BWB", "NGB", "DoA", + "CBFM", "PTer", + "rovers", "AREY", "CJWL", + "cakes", "HDH", + "PmP", + "SoM", + "PVR", + "NTG", "d3g", + "BAE", "TTG", "ANi", "TJUPT", "HYSUB", "KTXP", + "QPEL", + "M0RETV", + "FHD", + "WLM", + "NTb", "MCE", "DMG" ] diff --git a/soda_resource_tools_lib/config/mt_strong_match_rules.json b/soda_resource_tools_lib/config/mt_strong_match_rules.json deleted file mode 100644 index e63432ad..00000000 --- a/soda_resource_tools_lib/config/mt_strong_match_rules.json +++ /dev/null @@ -1,608 +0,0 @@ -{ - "before_replaces": [ - { - "src": ".£", - "target": "-" - }, - { - "src": "£", - "target": "-" - }, - { - "src": ".mp4.mkv", - "target": ".mkv" - } - ], - "rules": [ - { - "example": "无耻家庭.全十一季", - "rule": "$title_cn$.$season_all_cn$" - }, - { - "example": "S01-PTer", - "rule": "$season$-$release_group$" - }, - { - "example": "暗黑S01-S03.Dark.2017-2020.2160p.WEBrip.x265.AC3.£cXcY@FRDS", - "rule": "$title_cn$$season_start_to_end_en$.$title_en$.$year_start_to_end$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$" - }, - { - "example": "黑色五叶草:魔法帝之剑.Black.Clover.Sword.of.the.Wizard.King.2023.1080p.NF.WEB-DL.H264.DDP5.1-OurTV", - "rule": "$title_cn$.$title_en$.$year$.$resolution$.$company$.$source$.$video_codec$.$audio_codec$-$release_group$" - }, - { - "example": "邪恶力量.第01-14季.Supernatural.S01-S14.1080p.Blu-Ray.AC3.x265.10bit-Yumi", - "rule": "$title_cn$.$season_start_to_end_cn$.$title_en$.$season_start_to_end_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$" - }, - { - "example": "邪恶力量.第01季.Supernatural.S01.1080p.Blu-Ray.AC3.x265.10bit-Yumi", - "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$resolution$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$" - }, - { - "example": "无耻家庭.第01季.Shameless.S01.2011.1080p.AMZN.WEB-DL.AC3.x265.10bit-Yumi@FRDS", - "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$year$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$" - }, - { - "example": "生活大爆炸.第01季.The.Big.Bang.Theory.S01.2007.1080p.Bluray.AC3.x265.10bit-Yumi", - "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$" - }, - { - "example": "The.Office.US.S09E12.2012.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$country$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "A.Perfect.Planet.S01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD", - "rule": "$title_en$.$season$.$source$.$resolution$.$audio_codec$.$color$.$video_codec$.$color$-$release_group$" - }, - { - "example": "Alone.S01.1080p.AMZN.WEBRip.DDP2.0.x264-Cinefeel", - "rule": "$title_en$.$season$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$-$release_group$" - }, - { - "example": "A.Perfect.Planet.2021.2160p.BluRay.x265.10bit.Atmos.TrueHD7.1-WiKi", - "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$" - }, - { - "example": "Fair.Game.2010.1080p.Blu-ray.Ger.AVC.DTS-HD.MA.5.1-BHYS@OurBits", - "rule": "$title_en$.$year$.$resolution$.$source$.$country$.$video_codec$.$audio_codec$-$release_group$" - }, - { - "example": "Avatar.The.Way.of.Water.2022.BluRay.1080p.AVC.DTS-HD.MA5.1-loongkee@MTeam", - "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$-$release_group$" - }, - { - "example": "The.Last.of.Us.Game.CG.2160p.60fps.WEB-DL.H264.AAC-Stone", - "rule": "$title_en$.$resolution$.$color$.$source$.$video_codec$.$audio_codec$.$release_group$" - }, - { - "example": "John Wick Chapter 4 2023 UHD BluRay 2160p HEVC Atmos TrueHD7.1-BHYS@OurBits", - "rule": "$title_number_en$ $year$ $color$ $source$ $resolution$ $video_codec$ $audio_codec$-$release_group$" - }, - { - "example": "Attenborough 60 Years in the Wild 2012 2Disc Blu-ray 1080i AVC DTS-HD 2.0-TTG", - "rule": "$title_number_en$ $year$ $source$ $resolution$ $video_codec$ $audio_codec$-$release_group$", - "replaces": [ - { - "src": " 2Disc ", - "target": " " - } - ] - }, - { - "example": "Scarlet EP01.mp4", - "rule": "$title_en$ $episode$.$extension$" - }, - { - "example": "S01E01-PTer.mkv", - "rule": "$season_episode$-$release_group$.$extension$" - }, - { - "example": "[壹高清]你喜欢勃拉姆斯吗.Do You Like Brahms.Ep01.HDTV.720p.H264-OneHD.ts", - "rule": "[$company$]$title_cn$.$title_en$.$episode$.$source$.$resolution$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Jade.Forensic.Heroes.V.E01.2022.HDTV.1080i.H264-HDCTV.ts", - "rule": "$company$.$title_en$.$episode$.$year$.$source$.$resolution$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Jade.Other People's Money.Ep01.HDTV.1080p.H264-CNHK.ts", - "rule": "$company$.$title_en$.$episode$.$source$.$resolution$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "AOD.百万同居计划.Million Dollar Family.Ep01.HDTV.1080p.H264-OneHD.ts", - "rule": "$company$.$title_cn$.$title_en$.$episode$.$source$.$resolution$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "[三国].Three.Kingdoms.EP01.2010.BluRay.720p.x264.AC3-CMCT.mkv", - "rule": "[$title_cn$].$title_en$.$episode$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "[三体].Three-Body.2023.S01E01.2160p.V3.WEB-DL.HEVC.DDP5.1.Atmos.&.AAC-QHstudIo.mp4", - "rule": "[$title_cn$].$title_en$.$year$.$season_episode$.$resolution$.$version$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "[傲椒的湘菜].THE.PRIDE.OF.HUNAN.CUISINE.2021.EP01.WEB-DL.2160p.HEVC.AAC-QHstudIo.mp4", - "rule": "[$title_cn$].$title_en$.$year$.$episode$.$source$.$resolution$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "[大堡礁].BBC.Great.Barrier.Reef.2012.Blu-ray.1080i.AVC.DTS-HD.MA.2.0-CMCT.mkv", - "rule": "[$title_cn$].$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "[非洲].BBC.Africa.S01.2013.Blu-ray.1080i.AVC.DTS-HD.MA.5.1-CMCT.mkv", - "rule": "[$title_cn$].$title_en$.$season$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "汉尼拔S01-S03.Hannibal.2013-2015.1080p.Blu-ray.x265.AC3£cXcY@FRDS.mkv", - "rule": "$title_cn$$season_start_to_end_en$.$title_en$.$year_start_to_end$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "暗黑S01-S03.Dark.2017-2020.2160p.WEBrip.x265.AC3.£cXcY@FRDS.mkv", - "rule": "$title_cn$$season_start_to_end_en$.$title_en$.$year_start_to_end$.$resolution$.$source$.$video_codec$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "灿烂的转身.The.Magical.Women.S01E01.2023.2160p.WEB-DL.H265.DDP5.1-ADWeb.mkv", - "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "消失的十一层.The.Lost.11th.Floor.S01E24.2023.2160p.WEB-DL.H265.DDP5.1-OurTV.mp4", - "rule": "$title_cn$.$title_number_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "繁城之下.Ripe.Town.S01E12.2023.2160p.WEB-DL.H265.DV.DDP5.1-OurTV.mp4", - "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "鹊刀门传奇.Legend.of.the.Undercover.Chef.S01E01.2023.2160p.WEB-DL.DDP.AAC.DV.H265-HDSWEB.mp4", - "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$audio_codec$.$color$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "繁城之下.Ripe.Town.S01E12.2023.2160p.WEB-DL.AAC.H.265-HDSWEB.mp4", - "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "去有风的地方.Meet.Yourself.2023.S01E01.1080p.HDR.WEB-DL.H265.AAC-OurTV.mp4", - "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$resolution$.$color$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "他是谁.Who.Is.He.S01E01.2023.2160p.60FPS.HQ.WEB-DL.H265.10bit.AAC-OurTV.mp4", - "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$color$.$company$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "护心.Back.From.The.Brink.2023.EP03.WEB-DL.4K.H265.10bit.AAC.Mandarin-OPS.mp4", - "rule": "$title_cn$.$title_en$.$year$.$episode$.$source$.$resolution$.$video_codec$.$color$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "护心.Back From The Brink.S01E03.2023.2160p.HQ.WEB-DL.AAC.H265-HDSWEB.mp4", - "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "鹿鼎記.1998.S01E01.1080p.myTV-SUPER.WEB-DL.H265.AAC-ManGor@MTeam.mkv", - "rule": "$title_cn$.$year$.$season_episode$.$resolution$.$company$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "龙族.Dragon.Raja.2022.E00.1080p.WEB-DL.H264.AAC-OurTV.mp4", - "rule": "$title_cn$.$title_en$.$year$.$episode$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4", - "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "凡人修仙传.The.Mortal.Ascention.S01.2160p.WEB-DL.H264.AAC-OurTV.mp4", - "rule": "$title_cn$.$title_en$.$season$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.mp4", - "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$resolution$.$extension$" - }, - { - "example": "凡人修仙传.The.Mortal.Ascention.2020.S01E01-OurTV.mp4", - "rule": "$title_cn$.$title_en$.$year$.$season_episode$-$release_group$.$extension$" - }, - { - "example": "凡人修仙传.The.Mortal.Ascention.2020.S01E01.mp4", - "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$extension$" - }, - { - "example": "二十二.Twenty.Two.2015.4K.WEB-DL.H265.DDP2.0-PTerWEB.mp4", - "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "龙马精神.Ride.On.2023.2160p.WEB-DL.H265.10bit.DTS.5.1-OurTV.mp4", - "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "黑色五叶草:魔法帝之剑.Black.Clover.Sword.of.the.Wizard.King.2023.1080p.NF.WEB-DL.H264.DDP5.1-OurTV.mkv", - "rule": "$title_cn$.$title_en$.$year$.$resolution$.$company$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "无耻家庭.第01季.Shameless.S01.2011.1080p.AMZN.WEB-DL.AC3.x265.10bit-Yumi@FRDS.mkv", - "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$year$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$.$extension$" - }, - { - "example": "生活大爆炸.第01季.The.Big.Bang.Theory.S01.2007.1080p.Bluray.AC3.x265.10bit-Yumi.mkv", - "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$" - }, - { - "example": "生活大爆炸.第01季.The.Big.Bang.Theory.S01.2007.1080p.Bluray.AC3.x265.10bit-Yumi.mkv", - "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$.$extension$" - }, - { - "example": "康熙王朝.E01.2001.DVDRip.x264.AC3-CMCT.mkv", - "rule": "$title_cn$.$episode$.$year$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "白い巨塔.E01.2003.1080p.HDTV.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_cn$.$episode$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "V世代.Gen.V.S01E01.2023.1080p.AMZN.WEB-DL.H264.DDP5.1-OurTV.mkv", - "rule": "$title_number_cn$.$title_en$.$season_episode$.$year$.$resolution$.$company$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "[DMG] EDENS ZERO [BDRip][HEVC_FLAC][1080P](4ABDCA70).mkv", - "rule": "[$release_group$] $title_en$ [$source$][$video_codec$_$audio_codec$][$resolution$]($mix_numbers_letters$).$extension$" - }, - { - "example": "The.IT.Crowd.Manual.2014.720p.HDTV.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$special$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Hannibal.S01.2013.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$season$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Planet.Earth.II.S01.EP01.Islands.2016.2160p.BluRay.REMUX.HEVC.DTS-HD.MA.5.1-DVB@szsddqwx.mkv", - "rule": "$title_en$.$season$.$episode$.$episode_title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "A.Perfect.Planet.S01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD.mkv", - "rule": "$title_en$.$season$.$source$.$resolution$.$audio_codec$.$color$.$video_codec$.$color$-$release_group$.$extension$" - }, - { - "example": "A.Perfect.Planet.S01E01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD.mkv", - "rule": "$title_en$.$season_episode$.$source$.$resolution$.$audio_codec$.$color$.$video_codec$.$color$-$release_group$.$extension$" - }, - { - "example": "True.Blood.S01E01.Strange.Love.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$audio_codec$.$source$-$release_group$.$extension$" - }, - { - "example": "Moving.S01E01.Senior.Year.2160p.Disney+.WEB-DL.DDP.5.1.Atmos.DV.HDR.H.265-Beans@CHDBits.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$company$.$source$.$audio_codec$.$color$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Moving.S01E12.Partners.1080p.DSNP.WEB-DL.DDP5.1.Atmos.H.264-APEX.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "The.Wire.S01E01.1080p.Blu-ray.REMUX.AVC.DTS-HD.MA.5.1-HDH.mkv", - "rule": "$title_en$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Supernatural.S01E01.Pilot.1080p.BluRay.REMUX.VC-1.DD.5.1-EPSiLON.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Smallville.S01E01.Pilot.1080p.BluRay.10Bit.Dts-HDMa5.1.HEVC-d3g.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$color$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Prison.Break.S01E01.Pilot.1080p.BluRay.Dts-HDMa5.1.PiR8.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "Friends.S01E01.1080p.BluRay.Remux.AVC.AC3-WhaleHu.mkv", - "rule": "$title_en$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "Breaking.Bad.S02E01.2160p.WEB-DL.5xRus.Ukr.Eng.TrollUHD-ULTRAHDCLUB.mkv", - "rule": "$title_en$.$season_episode$.$resolution$.$source$.$release_group$.$extension$", - "replaces": [ - { - "src": "5xRus.Ukr.Eng.", - "target": "" - } - ] - }, - { - "example": "Big.Bet.S01E01.2022.Disney+.WEB-DL.4K.HEVC.HDR.DDP-HDCTV.mkv", - "rule": "$title_en$.$season_episode$.$year$.$company$.$source$.$resolution$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "American.Born.Chinese.S01E01.2023.Disney+.WEB-DL.1080p.H264.DDP-HDCTV.mkv", - "rule": "$title_en$.$season_episode$.$year$.$company$.$source$.$resolution$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Oh.No.Here.Comes.Trouble.S01E02.2023.1080p.WEB-DL.AAC.H264-JKCT.mkv", - "rule": "$title_en$.$season_episode$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "The.Pacific.War.in.Color.S01E01.An.Ocean.Apart.1080p.AMZN.WEB-DL.DDP2.0.H.264-KAIZEN.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Great.British.Railway.Journeys.S01E02.HDTV.720p.x264-CiA.mkv", - "rule": "$title_en$.$season_episode$.$source$.$resolution$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Skam.S1E01.WEBRip.1080P.不着调字幕组.mkv", - "rule": "$title_en$.$season_episode$.$source$.$resolution$.$release_group$.$extension$" - }, - { - "example": "Skam.S1E01.你看起来像个荡女彐.中挪字幕.WEBRip.1080P.不着调字幕组.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_cn$.$subtitle_cn$.$source$.$resolution$.$release_group$.$extension$" - }, - { - "example": "Skam.S2E02.你对一个朋友撒谎却怪罪于我.SweSub.1080p.WEB-DL.H264.mp4", - "rule": "$title_en$.$season_episode$.$episode_title_cn$.$subtitle_en$.$resolution$.$source$.$video_codec$.$extension$" - }, - { - "example": "The.Blood.of.Youth.S01E01.2022.2160p.WEB-DL.H265.AAC-HHWEB.mp4", - "example1": "Mayans.M.C.S01E10.2018.1080p.WEB-DL.x265.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Love.Death.&.Robots.S03E01.1080p.NF.WEB-DL.DDP5.1.Atmos.x264-PuTao.mkv", - "rule": "$title_en$.$season_episode$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Moving.S01E03.2160p.Disney+.WEB-DL.DDP.5.1.Atmos.DV.HDR.H.265-Beans@CHDBits.mkv", - "rule": "$title_en$.$season_episode$.$resolution$.$company$.$source$.$audio_codec$.$color$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Downton.Abbey.S01E01.2010.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Snowpiercer.S01E01.First.the.Weather.Changed.2160p.Netflix.WEB-DL.DDP.5.1.Atmos.HDR.H.265-HHWEB.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$.$extension$" - }, - { - "example": "Moving.S01E03.1+1.2160p.Disney+.WEB-DL.DDP.5.1.Atmos.DV.HDR.H.265-Beans@CHDBits.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$company$.$source$.$audio_codec$.$color$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "The.Boys.S03E01.2022.2160p.AMZN.WEB-DL.H265.10bit.HDR10+.DDP5.1-tryHARDER@Hares.mkv", - "rule": "$title_en$.$season_episode$.$year$.$resolution$.$company$.$source$.$video_codec$.$color$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Gen.V.S01E01.2023.1080p.AMZN.WEB-DL.H264.DDP5.1-OurTV.mkv", - "rule": "$title_en$.$season_episode$.$year$.$resolution$.$company$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Sweet.Home.S02E01.2020.1080p.NF.WEB-DL.DDP5.1.H264-HHWEB.mkv", - "rule": "$title_en$.$season_episode$.$year$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Yes,Minister.S03E08.1984.Party.Games.480p.DVDrip.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$season_episode$.$year$.$episode_title_en$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Shameless.S01E01.1080p.AMZN.WEB-DL.AC3.x265.10bit-Yumi@FRDS.mkv", - "rule": "$title_en$.$season_episode$.$resolution$.$company$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$.$extension$" - }, - { - "example": "The.Big.Bang.Theory.S01E01.Pilot.1080p.Bluray.AC3.x265.10bit-Yumi.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$color$-$release_group$.$extension$" - }, - { - "example": "Young.Sheldon.S05E01.One.Bad.Night.and.Chaos.of.Selfish.Desires.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$extension$" - }, - { - "example": "Arrow.S01E01.Pilot.Bluray.1080p.x265.10bit.DDP.5.1.MNHD-FRDS.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$source$.$resolution$.$video_codec$.$color$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "The.Last.of.Us.S01E00.Making.of.2023.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$season_episode$.$episode_title_en$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "SEAL.Team.S03E11E12.2019.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$season_episode_episode$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Friends.S10E17E18.1080p.BluRay.Remux.AVC.AC3-WhaleHu.mkv", - "rule": "$title_en$.$season_episode_episode$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Better Call Saul S01E01 2160p WEB-DL DTS x265-TrollUHD.mkv", - "rule": "$title_en$ $season_episode$ $resolution$ $source$ $audio_codec$ $video_codec$-$release_group$.$extension$" - }, - { - "example": "The Big Bang Theory S03E01 The Electric Can Opener Fluctuation 1080p BluRay Remux VC1 DD5.1-Gamma.mkv", - "rule": "$title_en$ $season_episode$ $episode_title_en$ $resolution$ $source$ $video_codec$ $audio_codec$-$release_group$.$extension$" - }, - { - "example": "The Sopranos S01E01 1080p Blu-ray Remux AVC DTS-HD MA 5.1.mkv", - "rule": "$title_en$ $season_episode$ $resolution$ $source$ $video_codec$ $audio_codec$.$extension$" - }, - { - "example": "My.Own.Swordsman.2006.S01E01.2160p.WEB-DL.H265.AAC-LeagueWEB.mp4", - "rule": "$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "America.the.Beautiful.2022.S01E01.1080p.WEB.h264-KOGi.mkv", - "rule": "$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Explore.with.the.Note.2019.E01.1080p.WEB-DL.H264.AAC-LeagueWEB.mp4", - "rule": "$title_en$.$year$.$episode$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$", - "replaces": [ - { - "src": ".Complete", - "target": "" - }, - { - "src": "Complete.", - "target": "" - } - ] - }, - { - "example": "Under.the.Microscope.2023.S01E01.WEB-DL.2160P.HEVC.AAC-CJWL.mp4", - "rule": "$title_en$.$year$.$season_episode$.$source$.$resolution$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Tour.de.France.Unchained.2023.S01E01.1080p.NF.WEB-DL.x264.DDP5.1.Atmos-PTerWEB.mkv", - "rule": "$title_en$.$year$.$season_episode$.$resolution$.$company$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Women.In.Shanghai.2018.E01.2160p.WEB-DL.H265.10bit.AAC-LeagueWEB.mp4", - "rule": "$title_en$.$year$.$episode$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Vesper.2022.2160p.WEB-DL.x265.10bit.HDR.DDP5.1-NOGRP.mkv", - "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Cast.Away.2000.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-BHYS@OurBits.iso", - "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Fair.Game.2010.1080p.Blu-ray.Ger.AVC.DTS-HD.MA.5.1-BHYS@OurBits.mkv", - "rule": "$title_en$.$year$.$resolution$.$source$.$country$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "New.Life.Begins.2022.S01E01.WEB-DL.4K.HDR.H265.10bit.DDP.AAC-HDCTV.mp4", - "rule": "$title_en$.$year$.$season_episode$.$source$.$resolution$.$color$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "One.Dream.One.Home.2019.E01.WEB-DL.4K.HLG10.H265.AAC-HDCTV.mp4", - "rule": "$title_en$.$year$.$episode$.$source$.$resolution$.$color$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Howl's.Moving.Castle.2004.BluRay.1080p.x265.10bit.4Audio.MNHD-FRDS.mkv", - "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$color$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "Avatar.The.Way.of.Water.2022.BluRay.1080p.AVC.DTS-HD.MA5.1-loongkee@MTeam.mkv", - "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Heidi.2015.BluRay.1080p.DTS-HD.MA.5.1.3Audio.x264-EPiC.mkv", - "rule": "$title_en$.$year$.$source$.$resolution$.$audio_codec$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "The.Big.Short.2015.BluRay.1080p.DTS-HD.MA.7.1.x265.10bit-CHD.mkv", - "rule": "$title_en$.$year$.$source$.$resolution$.$audio_codec$.$video_codec$.$color$-$release_group$.$extension$" - }, - { - "example": "Love.is.Pyjamas.2012.Blu-ray.1080p.Remux.AVC.TrueHD.7.1-Dream.mkv", - "rule": "$title_en$.$year$.$source$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "A.Better.Tomorrow.1986.UHD.BluRay.2160p.x265.SDR.DTS-HD.MA7.1.mUHD-FRDS.mkv", - "rule": "$title_en$.$year$.$color$.$source$.$resolution$.$video_codec$.$color$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "Downton.Abbey.A.New.Era.2022.2160p.WEB-DL.DDP5.1.Atmos.HDR.DV.x265-HDFans.mkv", - "rule": "$title_en$.$year$.$resolution$.$source$.$audio_codec$.$color$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "From.Dusk.Till.Dawn.1996.1080p.GER.Blu-ray.Hevc.10bit.DTS-HD.MA.5.1-Iceyumao@Dream.mkv", - "rule": "$title_en$.$year$.$resolution$.$country$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "A.Beautiful.Planet.2016.2160p.UHD.BluRay.HDR10+.HEVC.DTS-X-DiY@HDHome.mkv", - "rule": "$title_en$.$year$.$resolution$.$color$.$source$.$color$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Triangle.of.Sadness.2022.CC.UHD.BluRay.2160p.x265.10bit.HDR.mUHD-FRDS.mkv", - "rule": "$title_en$.$year$.$company$.$color$.$source$.$resolution$.$video_codec$.$color$.$color$.$release_group$.$extension$" - }, - { - "example": "A.Perfect.Planet.E01.Volcano.2021.2160p.BluRay.x265.10bit.Atmos.TrueHD7.1-WiKi.mkv", - "rule": "$title_en$.$episode$.$episode_title_en$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Wolf.Hall.E01.2015.1080p.Blu-ray.x265.10bit.DTS£cXcY@FRDS.mkv", - "rule": "$title_en$.$episode$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "The.Long.Season.E12.2023.1080p.WEBrip.NF.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$episode$.$year$.$resolution$.$source$.$company$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "Empresses.in.the.Palace.E01.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", - "rule": "$title_en$.$episode$.$source$.$resolution$.$video_codec$.$color$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "The.Last.of.Us.Game.CG.2160p.60fps.WEB-DL.H264.AAC-Stone.mkv", - "rule": "$title_en$.$resolution$.$color$.$source$.$video_codec$.$audio_codec$.$release_group$.$extension$" - }, - { - "example": "[cX]A.Small.Light.E08.2023.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", - "rule": "$title_en$.$episode$.$year$.$resolution$.$source$.$video_codec$.$color$.$audio_codec$-$release_group$.$extension$", - "replaces": [ - { - "src": "[cX]", - "target": "" - } - ] - }, - - { - "example": "1899.2022.S01E01.1080p.NF.WEB-DL.H264.DDP5.1.Atmos-OurTV.mkv", - "rule": "$title_year$.$year$.$season_episode$.$resolution$.$company$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$" - }, - { - "example": "13.Reasons.Why.S01E01.Tape.1.Side.A.2160p.NF.WEB-DL.DDP5.1.HDR.HEVC-HHWEB.mkv", - "rule": "$title_number_en$.$season_episode$.$episode_title_en$.$resolution$.$company$.$source$.$audio_codec$.$color$.$video_codec$-$release_group$.$extension$" - }, - { - "example": "Prince of Persia the Sands of Time 2010 BluRay 1080p x265 2Audio-WGXC@HDFans.mkv", - "rule": "$title_en$ $year$ $source$ $resolution$ $video_codec$ $audio_codec$-$release_group$.$extension$" - }, - { - "example": "(2010.05.26)Evangelion 2.22 You Can (Not) Advance-[1080p][JP.BD.Remux].mkv", - "rule": "($year_month_day$)$title_number_en$-[$resolution$][$country$.$source$].$extension$" - }, - { - "example": "(2022.04.29)Re cycle of the Penguindrum Zenpen-[1080p][JP.BD.Remux][lolice-mix].mkv", - "rule": "($year_month_day$)$title_number_en$-[$resolution$][$country$.$source$][$release_group$].$extension$" - }, - { - "example": "Aharen-san wa Hakarenai S01E01-[1080p][JP.BD.Remux][lolice-mix].mkv", - "rule": "$title_en$ $season_episode$-[$resolution$][$country$.$source$][$release_group$].$extension$" - }, - { - "example": "Jujutsu Kaisen 0-[1080p][JP.BD.Remux][lolice-mix].mkv", - "rule": "$title_number_en$-[$resolution$][$country$.$source$][$release_group$].$extension$" - }, - { - "example": "[DMG] EDENS ZERO Menu Vol.1 [BDRip][HEVC_FLAC][1080P_Ma10P](4ABDCA70).mkv", - "rule": "[$release_group$] $title_en$ [$source$][$video_codec$_$audio_codec$][$resolution$_$color$]($mix_numbers_letters$).$extension$", - "replaces": [ - { - "src": "Menu Vol.1 ", - "target": "" - } - ] - }, - { - "example": "Eiyuuoh Bu Wo S01E01-[1080p][BDRIP][x265.FLAC].mkv", - "rule": "$title_en$ $season_episode$-[$resolution$][$source$][$video_codec$.$audio_codec$].$extension$" - }, - { - "example": "[Isekai Ojisan][Vol.01][Menu][BDRIP][1080P][H264_FLAC].mkv", - "rule": "[$title_en$][$source$][$resolution$][$video_codec$_$audio_codec$].$extension$", - "replaces": [ - { - "src": "[Vol.01]", - "target": "" - }, - { - "src": "[Menu]", - "target": "" - } - ] - }, - { - "example": "[Isekai Ojisan][03][BDRIP][1080P][H264_FLAC].mkv", - "rule": "[$title_en$][$episode_number$][$source$][$resolution$][$video_codec$_$audio_codec$].$extension$" - } - ] -} \ No newline at end of file diff --git a/soda_resource_tools_lib/config/mt_strong_match_rules_movie.json b/soda_resource_tools_lib/config/mt_strong_match_rules_movie.json new file mode 100644 index 00000000..71b98d27 --- /dev/null +++ b/soda_resource_tools_lib/config/mt_strong_match_rules_movie.json @@ -0,0 +1,390 @@ +{ + "before_replaces": [ + { + "src": "-高码率", + "target": "" + }, + { + "src": "Top138.", + "target": "" + }, + { + "src": "(4K修复版)", + "target": "" + } + ], + "rules": [ + { + "example": "风味人间3·大海小鲜.Once.Upon.A.Bite.2021.S03E01.4K.WEB-DL.H265.DoVi.DDP5.1.Atmos-PTerWEB.mp4", + "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "电锯惊魂10.saw.x.2023.1080p.WEB-DL.DD5.1.H264.mkv", + "rule": "$title_number_cn$.$title_en$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "风味人间4.Once.Upon.A.Bite.2022.S04E01.2160p.50FPS.WEB-DL.H265.DV.DDP2.0-OurTV.mp4", + "rule": "$title_number_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "请回答1988.第20集.Reply.1988.UHDTV.60fps.DD2.0.x265.10bit-Yumi.mkv", + "rule": "$title_number_cn$.$title_number_en$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "24小时.S01E01.2001.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_number_cn$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "(2022.04.29)Re cycle of the Penguindrum Zenpen-[1080p][JP.BD.Remux][lolice-mix].mkv", + "rule": "($year_month_day$)$title_number_en$-[$resolution$][$source$].$extension$" + }, + { + "example": "Jujutsu Kaisen 0-[1080p][JP.BD.Remux][lolice-mix].mkv", + "rule": "$title_number_en$-[$resolution$][$source$].$extension$" + }, + { + "example": "黑色五叶草:魔法帝之剑.Black.Clover.Sword.of.the.Wizard.King.2023.1080p.NF.WEB-DL.H264.DDP5.1-OurTV", + "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$" + }, + { + "example": "The.Office.US.S09E12.2012.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Shameless.US.S09E01.1080p.AMZN.WEB-DL.AC3.x265.10bit-Yumi@FRDS.mkv", + "rule": "$title_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "[壹高清]你喜欢勃拉姆斯吗.Do You Like Brahms.Ep01.HDTV.720p.H264-OneHD.ts", + "rule": "$title_cn$.$title_en$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "[三国].Three.Kingdoms.EP01.2010.BluRay.720p.x264.AC3-CMCT.mkv", + "rule": "[$title_cn$].$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[三体].Three-Body.2023.S01E01.2160p.V3.WEB-DL.HEVC.DDP5.1.Atmos.&.AAC-QHstudIo.mp4", + "rule": "[$title_cn$].$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[大明王朝1566].Da.Ming.Wang.Chao.2007.E01.1080p.WEB-DL.H265.AAC-PTerWEB.mp4", + "rule": "[$title_number_cn$].$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Jade.Forensic.Heroes.V.E01.2022.HDTV.1080i.H264-HDCTV.ts", + "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "Wonders.Of.The.Solar.System.EP01.Empire.of.the.Sun.2010.1080p.BluRay.DD2.0.x264-HDS.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "TVB Anywhere.法证先锋V.Forensic Heroes V.Ep01.HDTV.1080p.H264.2Audio-OneHD.mkv", + "rule": "$title_cn$.$title_en$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "消失的十一层.The.Lost.11th.Floor.S01E24.2023.2160p.WEB-DL.H265.DDP5.1-OurTV.mp4", + "rule": "$title_cn$.$title_number_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "鹊刀门传奇.Legend.of.the.Undercover.Chef.S01E01.2023.2160p.WEB-DL.DDP.AAC.DV.H265-HDSWEB.mp4", + "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "鹿鼎記.1998.S01E01.1080p.myTV-SUPER.WEB-DL.H265.AAC-ManGor@MTeam.mkv", + "rule": "$title_cn$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "护心.Back.From.The.Brink.2023.EP03.WEB-DL.4K.H265.10bit.AAC.Mandarin-OPS.mp4", + "rule": "$title_cn$.$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "凡人修仙传.The.Mortal.Ascention.S01.2160p.WEB-DL.H264.AAC-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.mp4", + "rule": "$title_cn$.$title_en$.$year$.$resolution$.$extension$" + }, + { + "example": "凡人修仙传.The.Mortal.Ascention.2020.S01E01-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$year$.$extension$" + }, + { + "example": "康熙王朝.E01.2001.DVDRip.x264.AC3-CMCT.mkv", + "rule": "$title_cn$.$year$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "A.Perfect.Planet.S01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD.mkv", + "rule": "$title_en$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "True.Blood.S01E01.Strange.Love.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR.mkv", + "rule": "$title_en$.$resolution$.$audio_codec$.$source$.$extension$" + }, + { + "example": "Supernatural.S01E01.Pilot.1080p.BluRay.REMUX.VC-1.DD.5.1-EPSiLON.mkv", + "rule": "$title_en$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "What.on.Earth.S05E01.CIA.Killer.Monks.1080p.WEB.x264-CAFFEiNE.mkv", + "rule": "$title_en$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Prison.Break.S01E01.Pilot.1080p.BluRay.Dts-HDMa5.1.PiR8.mkv", + "rule": "$title_en$.$resolution$.$source$.$audio_codec$.$extension$" + }, + { + "example": "Breaking.Bad.S02E01.2160p.WEB-DL.5xRus.Ukr.Eng.TrollUHD-ULTRAHDCLUB.mkv", + "rule": "$title_en$.$resolution$.$source$.$extension$", + "replaces": [ + { + "src": "5xRus.Ukr.Eng.", + "target": "" + } + ] + }, + { + "example": "Big.Bet.S01E01.2022.Disney+.WEB-DL.4K.HEVC.HDR.DDP-HDCTV.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Hanzawa.Naoki.S01E01.2013.DC.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$year$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Great.British.Railway.Journeys.S01E02.HDTV.720p.x264-CiA.mkv", + "rule": "$title_en$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "Skam.S1E01.WEBRip.1080P.不着调字幕组.mkv", + "rule": "$title_en$.$source$.$resolution$.$extension$" + }, + { + "example": "The.Universe.S01E01.Secrets.of.the.Sun.2007.BD-REMUX.1080P.H264.AC3-FrankB.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Arrow.S01E01.Pilot.Bluray.1080p.x265.10bit.DDP.5.1.MNHD-FRDS.mkv", + "rule": "$title_en$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Supernatural.S06E22.The.Man.Who.Knew.Too.Much.Blu-Ray.AC3.x265.10bit-Yumi.mkv", + "rule": "$title_en$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "The.Universe.S05E01.7.Wonders.of.the.Solar.System.BD-REMUX.1080P.H264.AVC.DTS-HD-FrankB.mkv", + "rule": "$title_en$.$source$.$resolution$.$video_codec$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Universe.S09E01.Omens.of.Doom.1080p.x264.Eac3-FrankB.mkv", + "rule": "$title_en$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Universe.S08E01.2014.stonehenge.1080p.BD-REMUX.H264.AVC.DTS-FrankB.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "America.the.Beautiful.2022.S01E01.1080p.WEB.h264-KOGi.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Fringe.2008.S01E01.Blu-Ray.1080p.DD5.1.x265.10bit-Yumi.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Love.is.Pyjamas.2012.Blu-ray.1080p.Remux.AVC.TrueHD.7.1-Dream.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "1899.2022.S01E01.1080p.NF.WEB-DL.H264.DDP5.1.Atmos-OurTV.mkv", + "rule": "$title_year$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Prince of Persia the Sands of Time 2010 BluRay 1080p x265 2Audio-WGXC@HDFans.mkv", + "rule": "$title_en$ $year$ $source$ $resolution$ $video_codec$ $audio_codec$.$extension$", + "replaces": [ + { + "src": " Extended ", + "target": " " + } + ] + }, + { + "example": "[DMG] EDENS ZERO Menu Vol.1 [BDRip][HEVC_FLAC][1080P_Ma10P](4ABDCA70).mkv", + "rule": "$title_en$ [$source$][$video_codec$_$audio_codec$][$resolution$]($mix_numbers_letters$).$extension$", + "replaces": [ + { + "src": "Menu Vol.1 ", + "target": "" + } + ] + }, + { + "example": "[Isekai Ojisan][Vol.01][Menu][BDRIP][1080P][H264_FLAC].mkv", + "rule": "[$title_en$][$source$][$resolution$][$video_codec$_$audio_codec$].$extension$", + "replaces": [ + { + "src": "[Vol.01]", + "target": "" + }, + { + "src": "[Menu]", + "target": "" + } + ] + }, + { + "example": "2.Broke.Girls.S01E01.2011.1080p.WEB-DL.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$number$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "早餐中国.Breakfast.In.China.S04E31.2020.2160p.WEB-DL.H265.mkv", + "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "BBC.Attenborough.60.Years.in.the.Wild.2012.EP01.BluRay.1080p.DTS-HD.MA.2.0.x265.10bit-BeiTai.mkv", + "rule": "$title_number_en$.$year$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Around.The.World.In.80.Gardens.2008.E10.1080p.WEB-DL.H264.AAC-PTerWEB.mp4", + "rule": "$title_number_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Around.The.World.In.80.Gardens.2008.E10.1080p.WEB-DL.H264.AAC-PTerWEB.mp4", + "rule": "$title_number_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "John Wick Chapter 4 2023 UHD BluRay 2160p HEVC Atmos TrueHD7.1-BHYS@OurBits", + "rule": "$title_number_en$ $year$ $color$ $source$ $resolution$ $video_codec$ $audio_codec$" + }, + { + "example": "Attenborough 60 Years in the Wild 2012 2Disc Blu-ray 1080i AVC DTS-HD 2.0-TTG", + "rule": "$title_number_en$ $year$ $source$ $resolution$ $video_codec$ $audio_codec$" + }, + { + "example": "13.Reasons.Why.S01E01.Tape.1.Side.A.2160p.NF.WEB-DL.DDP5.1.HDR.HEVC-HHWEB.mkv", + "rule": "$title_number_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Record.of.Ragnarok.II.2023.S02E01.1080p.NF.WEB-DL.x264.DDP2.0-PTerWEB.mkv", + "rule": "$title_en$.$year$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "[VCB-Studio] OVERLORD [13][1080p][x265_flac].mkv", + "rule": "$title_en$ [$resolution$][$video_codec$_$audio_codec$].$extension$" + }, + { + "example": "[VCB-Studio] Durarara!!×2 Shou [01][Ma10p_1080p][x265_flac_aac].mkv", + "rule": "$title_number_en$ [$resolution$][$video_codec$_$audio_codec$].$extension$" + }, + { + "example": "Dragon.Ball.150.480p.x264.mkv", + "rule": "$title_en$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "[sergey_krs] Kuroshitsuji II - 01 [BDRip 1920x1080 x264 FLAC].mkv", + "rule": "[$title_en$][$source$][$audio_codec$][$resolution$].$extension$" + }, + { + "example": "扫毒3:人在天涯.The.White.Storm.3.Heaven.or.Hell.2023.2160p.WEB-DL.H265.10bit.DDP5.1.2Audio-OurTV.mp4", + "rule": "$title_number_cn$.$title_number_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "扫毒3:人在天涯.The.White.Storm.3.Heaven.or.Hell.2023.2160p.WEB-DL.H265.10bit.DDP5.1.2Audio-OurTV.mp4", + "rule": "$title_number_cn$.$title_number_en$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "icky Cristina Barcelona 2008 1080p Bluray Remux VC-1 DTS-HD 3.0-decatora27.mkv", + "rule": "$title_en$ $year$ $resolution$ $source$ $video_codec$ $audio_codec$.$extension$", + "replaces": [ + { + "src": " REMUX ", + "target": " " + } + ] + }, + { + "example": "icky Cristina Barcelona 2008 1080p Bluray Remux VC-1 DTS-HD 3.0-decatora27.mkv", + "rule": "$title_en$ $year$ $resolution$ $source$ $audio_codec$ $video_codec$.$extension$" + }, + { + "example": "Isle Of Dogs 2018 BluRay 1080p HEVC DTS-HD MA 5.1 x265.mkv", + "rule": "$title_en$ $year$ $source$ $resolution$ $video_codec$ $audio_codec$ $video_codec$.$extension$" + }, + { + "example": "Oceans 2009.CHN.Bluray.1080p.x265.10bit.2Audios.MNHD-FRDS.mkv", + "rule": "$title_en$ $year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "12.Angry.Men.1957.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "rule": "$title_number_en$.$year$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "The_Last_Thing_He_Wanted_2020_INTERNAL_1080p_WEB_X264-AMRAP.mkv", + "rule": "$title_en$.$year$.$company$.$resolution$.$source$.$video_codec$.$extension$", + "replaces": [ + { + "src": "_", + "target": "." + } + ] + }, + { + "example": "Phantoms 1998 BluRay 1080p AC3 HEVC-d3g.mkv", + "rule": "$title_en$ $year$ $source$ $resolution$ $audio_codec$ $video_codec$.$extension$" + }, + { + "example": "Suzume.2022.1080p.iTunes.2Audio.WEB-DL.DD5.1.H.264.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$audio_codec$.video_codec$.$extension$" + }, + { + "example": "Wreck-It.Ralph2012.Bluray.1080p.x265.10bit.4Audios.MNHD-FRDS.mkv", + "rule": "$title_en$$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "New.Dragon.Inn.1992.1080p.BluRay.1080p.x265.2Audio.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "New.Dragon.Inn.1992.1080p.BluRay.1080p.x265.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "The.Blind.Side.2009.BluRay.720p.MNHD-FRDS.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$extension$" + }, + { + "example": "The.Great.Buddha+2017.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "rule": "$title_en$\\+$year$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "Edward.Scissorhands.25th.Anniversary.Edition.BluRay.1080p.x265.2Audios.mkv", + "rule": "$title_number_en$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Edward.Scissorhands.25th.Anniversary.Edition.BluRay.1080p.x265.2Audios.mkv", + "rule": "$title_number_en$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Dazed.and.Confused.1993.1080p.BluRay.REMUX.AVC-DTS-HD.MA.5.1.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$-$audio_codec$.$extension$" + }, + { + "example": " The.Town.2010.BluRay.1080p.DTSHD-MA.h264.Remux.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$audio_codec$.$video_codec$.$source$.$extension$" + }, + { + "example": "Terminator.2.Judgment.Day.1991.1080p.Blu-ray.x265.DTS-HD.mkv", + "rule": "$title_number_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Bourne.Identity.2002.1080p.Blu-ray.x265.10bit.DTS£cXcY@FRDS(1.58.17).mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$($mix_numbers_letters$).$extension$" + }, + { + "example": "Home.Alone.3.1997.1080p.WEB.x264.mkv", + "rule": "$title_number_en$.$year$.$resolution$.$source$.$video_codec$.$extension$" + } + ] +} \ No newline at end of file diff --git a/soda_resource_tools_lib/config/mt_strong_match_rules_tv.json b/soda_resource_tools_lib/config/mt_strong_match_rules_tv.json new file mode 100644 index 00000000..3c918a98 --- /dev/null +++ b/soda_resource_tools_lib/config/mt_strong_match_rules_tv.json @@ -0,0 +1,876 @@ +{ + "before_replaces": [ + { + "src": " - ", + "target": " " + }, + { + "src": ".£", + "target": "-" + }, + { + "src": "£", + "target": "-" + }, + { + "src": "..", + "target": "." + }, + { + "src": ".mp4.mkv", + "target": ".mkv" + }, + { + "src": ".mp4.mp4", + "target": ".mp4" + }, + { + "src": " .mkv", + "target": ".mkv" + }, + { + "src": ".REPACK.", + "target": "." + }, + { + "src": "内嵌", + "target": "" + }, + { + "src": "内封", + "target": "" + }, + { + "src": ".Complete.", + "target": "." + }, + { + "src": ".COMPLETE.", + "target": "." + }, + { + "src": " 2Disc ", + "target": " " + }, + { + "src": ".MultiAudio.", + "target": "." + }, + { + "src": " D1 ", + "target": " " + }, + { + "src": ".dv.", + "target": "." + }, + { + "src": ".iP.", + "target": "." + }, + { + "src": " ", + "target": " " + }, + { + "src": " - ", + "target": " " + }, + { + "src": " ", + "target": " " + }, + { + "src": " ", + "target": " " + }, + { + "src": " .mkv", + "target": ".mkv" + }, + { + "src": "Blue-ray", + "target": "BluRay" + }, + { + "src": ".[v2].", + "target": "." + }, + { + "src": " v2", + "target": "" + } + ], + "rules": [ + { + "example": "风味人间3·大海小鲜.Once.Upon.A.Bite.2021.S03E01.4K.WEB-DL.H265.DoVi.DDP5.1.Atmos-PTerWEB.mp4", + "rule": "$title_cn$$season_number$·$season_title_cn$.$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "风味人间4.Once.Upon.A.Bite.2022.S04E01.2160p.50FPS.WEB-DL.H265.DV.DDP2.0-OurTV.mp4", + "rule": "$title_cn$$season_number$.$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "风味人间4.Once.Upon.A.Bite.2022.S04E01.2160p.50FPS.WEB-DL.H265.DV.DDP2.0-OurTV.mp4", + "rule": "$title_number_cn$.$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "请回答1988.第20集.Reply.1988.UHDTV.60fps.DD2.0.x265.10bit-Yumi.mkv", + "rule": "$title_number_cn$.$episode_cn$.$title_number_en$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "V世代.Gen.V.S01E01.2023.1080p.AMZN.WEB-DL.H264.DDP5.1-OurTV.mkv", + "rule": "$title_number_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "24小时.S01E01.2001.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_number_cn$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Aharen-san wa Hakarenai S01E01-[1080p][JP.BD.Remux][lolice-mix].mkv", + "rule": "$title_en$ $season_episode$-[$resolution$][$source$].$extension$" + }, + { + "example": "Eiyuuoh Bu Wo S01E01-[1080p][BDRIP][x265.FLAC].mkv", + "rule": "$title_en$ $season_episode$-[$resolution$][$source$][$video_codec$.$audio_codec$].$extension$" + }, + { + "example": "(2022.04.29)Re cycle of the Penguindrum Zenpen-[1080p][JP.BD.Remux][lolice-mix].mkv", + "rule": "($year_month_day$)$title_number_en$-[$resolution$][$source$].$extension$" + }, + { + "example": "(2010.05.26)Evangelion 2.22 You Can (Not) Advance-[1080p][JP.BD.Remux].mkv", + "rule": "($year_month_day$)$title_number_en$-[$resolution$][$source$].$extension$" + }, + { + "example": "Jujutsu Kaisen 0-[1080p][JP.BD.Remux][lolice-mix].mkv", + "rule": "$title_number_en$-[$resolution$][$source$].$extension$" + }, + { + "example": "无耻家庭.全十一季", + "rule": "$title_cn$.$season_all_cn$" + }, + { + "example": "S01-PTer", + "rule": "$season$" + }, + { + "example": "暗黑S01-S03.Dark.2017-2020.2160p.WEBrip.x265.AC3.£cXcY@FRDS", + "rule": "$title_cn$$season_start_to_end_en$.$title_en$.$year_start_to_end$.$resolution$.$source$.$video_codec$.$audio_codec$" + }, + { + "example": "黑色五叶草:魔法帝之剑.Black.Clover.Sword.of.the.Wizard.King.2023.1080p.NF.WEB-DL.H264.DDP5.1-OurTV", + "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$" + }, + { + "example": "邪恶力量.第01-14季.Supernatural.S01-S14.1080p.Blu-Ray.AC3.x265.10bit-Yumi", + "rule": "$title_cn$.$season_start_to_end_cn$.$title_en$.$season_start_to_end_en$.$resolution$.$source$.$audio_codec$.$video_codec$" + }, + { + "example": "邪恶力量.第01季.Supernatural.S01.1080p.Blu-Ray.AC3.x265.10bit-Yumi", + "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$resolution$.$source$.$audio_codec$.$video_codec$" + }, + { + "example": "无耻家庭.第01季.Shameless.S01.2011.1080p.AMZN.WEB-DL.AC3.x265.10bit-Yumi@FRDS", + "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$" + }, + { + "example": "A.Perfect.Planet.S01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD", + "rule": "$title_en$.$season$.$source$.$resolution$.$audio_codec$.$video_codec$" + }, + { + "example": "Alone.S01.1080p.AMZN.WEBRip.DDP2.0.x264-Cinefeel", + "rule": "$title_en$.$season$.$resolution$.$source$.$audio_codec$.$video_codec$" + }, + { + "example": "A.Perfect.Planet.2021.2160p.BluRay.x265.10bit.Atmos.TrueHD7.1-WiKi", + "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$" + }, + { + "example": "Avatar.The.Way.of.Water.2022.BluRay.1080p.AVC.DTS-HD.MA5.1-loongkee@MTeam", + "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$" + }, + { + "example": "The.Last.of.Us.Game.CG.2160p.60fps.WEB-DL.H264.AAC-Stone", + "rule": "$title_en$.$resolution$.$source$.$video_codec$.$audio_codec$" + }, + { + "example": "Lost.S01-S06.COMPLETE.2004-2010.1080p.Blu-ray.x265.10bits.DTS.5.1-PTer", + "rule": "$title_en$.$season_start_to_end_en$.$year_start_to_end$.$resolution$.$source$.$video_codec$.$audio_codec$" + }, + { + "example": "赘婿.第1季.My.Heroic.Husband.S01E36.2021.2160p.WEB-DL.H265.AAC-FLTTH.mp4", + "rule": "$title_cn$.$season_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Office.US.S09E12.2012.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Shameless.US.S09E01.1080p.AMZN.WEB-DL.AC3.x265.10bit-Yumi@FRDS.mkv", + "rule": "$title_en$.$season_episode$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Scarlet EP01.mp4", + "rule": "$title_en$ $episode$.$extension$" + }, + { + "example": "Scarlet EP50 End.mp4", + "rule": "$title_en$ $episode$ End.$extension$" + }, + { + "example": "S01E01-PTer.mkv", + "rule": "$season_episode$.$extension$" + }, + { + "example": "[壹高清]你喜欢勃拉姆斯吗.Do You Like Brahms.Ep01.HDTV.720p.H264-OneHD.ts", + "rule": "$title_cn$.$title_en$.$episode$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "[壹高清]你喜欢勃拉姆斯吗.Do You Like Brahms.Ep16.End.HDTV.720p.H264-OneHD.ts", + "rule": "$title_cn$.$title_en$.$episode$.$episode_title_en$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "[三国].Three.Kingdoms.EP01.2010.BluRay.720p.x264.AC3-CMCT.mkv", + "rule": "[$title_cn$].$title_en$.$episode$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[三体].Three-Body.2023.S01E01.2160p.V3.WEB-DL.HEVC.DDP5.1.Atmos.&.AAC-QHstudIo.mp4", + "rule": "[$title_cn$].$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[傲椒的湘菜].THE.PRIDE.OF.HUNAN.CUISINE.2021.EP01.WEB-DL.2160p.HEVC.AAC-QHstudIo.mp4", + "rule": "[$title_cn$].$title_en$.$year$.$episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[大明王朝1566].Da.Ming.Wang.Chao.2007.E01.1080p.WEB-DL.H265.AAC-PTerWEB.mp4", + "rule": "[$title_number_cn$].$title_en$.$year$.$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[大堡礁].BBC.Great.Barrier.Reef.2012.Blu-ray.1080i.AVC.DTS-HD.MA.2.0-CMCT.mkv", + "rule": "[$title_cn$].$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[非洲].BBC.Africa.S01.2013.Blu-ray.1080i.AVC.DTS-HD.MA.5.1-CMCT.mkv", + "rule": "[$title_cn$].$title_en$.$season$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[信号].Signal.S01E01.2016.BluRay.1080p.x264.DTS-CMCT.mkv", + "rule": "[$title_cn$].$title_en$.$season_episode$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Jade.Forensic.Heroes.V.E01.2022.HDTV.1080i.H264-HDCTV.ts", + "rule": "$title_en$.$episode$.$year$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "Jade.Other People's Money.Ep01.HDTV.1080p.H264-CNHK.ts", + "rule": "$title_en$.$episode$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "BBC.Africa.EP01.Kalahari.2013.1080p.BluRay.x264.DTS-WiKi.mkv", + "rule": "$title_en$.$episode$.$episode_title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Wonders.Of.The.Solar.System.EP01.Empire.of.the.Sun.2010.1080p.BluRay.DD2.0.x264-HDS.mkv", + "rule": "$title_en$.$episode$.$episode_title_en$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "TVB Anywhere.法证先锋V.Forensic Heroes V.Ep01.HDTV.1080p.H264.2Audio-OneHD.mkv", + "rule": "$title_cn$.$title_en$.$episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "TVB Anywhere.法证先锋V.Forensic Heroes V.Ep01.Repack.HDTV.1080p.H264.2Audio-OneHD.mkv", + "rule": "$title_cn$.$title_en$.$episode$.$episode_title_en$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "汉尼拔S01-S03.Hannibal.2013-2015.1080p.Blu-ray.x265.AC3£cXcY@FRDS.mkv", + "rule": "$title_cn$$season_start_to_end_en$.$title_en$.$year_start_to_end$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "灿烂的转身.The.Magical.Women.S01E01.2023.2160p.WEB-DL.H265.DDP5.1-ADWeb.mkv", + "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "消失的十一层.The.Lost.11th.Floor.S01E24.2023.2160p.WEB-DL.H265.DDP5.1-OurTV.mp4", + "rule": "$title_cn$.$title_number_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "繁城之下.Ripe.Town.S01E12.2023.2160p.WEB-DL.H265.DV.DDP5.1-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "鹊刀门传奇.Legend.of.the.Undercover.Chef.S01E01.2023.2160p.WEB-DL.DDP.AAC.DV.H265-HDSWEB.mp4", + "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "繁城之下.Ripe.Town.S01E12.2023.2160p.WEB-DL.AAC.H.265-HDSWEB.mp4", + "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "去有风的地方.Meet.Yourself.2023.S01E01.1080p.HDR.WEB-DL.H265.AAC-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "他是谁.Who.Is.He.S01E01.2023.2160p.60FPS.HQ.WEB-DL.H265.10bit.AAC-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "鹿鼎記.1998.S01E01.1080p.myTV-SUPER.WEB-DL.H265.AAC-ManGor@MTeam.mkv", + "rule": "$title_cn$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "护心.Back.From.The.Brink.2023.EP03.WEB-DL.4K.H265.10bit.AAC.Mandarin-OPS.mp4", + "rule": "$title_cn$.$title_en$.$year$.$episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "龙族.Dragon.Raja.2022.E00.1080p.WEB-DL.H264.AAC-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$year$.$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "士兵突击.Soldiers.Sortie.2006.E01.2160p.WEB-DL.AAC.H.265-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$year$.$episode$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "凡人修仙传.The.Mortal.Ascention.S01.2160p.WEB-DL.H264.AAC-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$season$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.mp4", + "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$resolution$.$extension$" + }, + { + "example": "凡人修仙传.The.Mortal.Ascention.2020.S01E01-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$extension$" + }, + { + "example": "二十二.Twenty.Two.2015.4K.WEB-DL.H265.DDP2.0-PTerWEB.mp4", + "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "龙马精神.Ride.On.2023.2160p.WEB-DL.H265.10bit.DTS.5.1-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "无耻家庭.第01季.Shameless.S01.2011.1080p.AMZN.WEB-DL.AC3.x265.10bit-Yumi@FRDS.mkv", + "rule": "$title_cn$.$season_cn$.$title_en$.$season$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "康熙王朝.E01.2001.DVDRip.x264.AC3-CMCT.mkv", + "rule": "$title_cn$.$episode$.$year$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "白い巨塔.E01.2003.1080p.HDTV.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_cn$.$episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[DMG] EDENS ZERO [BDRip][HEVC_FLAC][1080P](4ABDCA70).mkv", + "rule": "$title_en$ [$source$][$video_codec$_$audio_codec$][$resolution$]($mix_numbers_letters$).$extension$" + }, + { + "example": "The.IT.Crowd.Manual.2014.720p.HDTV.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Hannibal.S01.2013.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$season$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Planet.Earth.II.S01.EP01.Islands.2016.2160p.BluRay.REMUX.HEVC.DTS-HD.MA.5.1-DVB@szsddqwx.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "A.Perfect.Planet.S01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD.mkv", + "rule": "$title_en$.$season$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "A.Perfect.Planet.S01E01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD.mkv", + "rule": "$title_en$.$season_episode$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "True.Blood.S01E01.Strange.Love.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$audio_codec$.$source$.$extension$" + }, + { + "example": "Moving.S01E01.Senior.Year.2160p.Disney+.WEB-DL.DDP.5.1.Atmos.DV.HDR.H.265-Beans@CHDBits.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Supernatural.S01E01.Pilot.1080p.BluRay.REMUX.VC-1.DD.5.1-EPSiLON.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "What.on.Earth.S05E01.CIA.Killer.Monks.1080p.WEB.x264-CAFFEiNE.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Prison.Break.S01E01.Pilot.1080p.BluRay.Dts-HDMa5.1.PiR8.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$extension$" + }, + { + "example": "The.Wire.S01E01.1080p.Blu-ray.REMUX.AVC.DTS-HD.MA.5.1-HDH.mkv", + "rule": "$title_en$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Walking.Dead.S01E01.1080p.BluRay.x264-Japhson.mkv", + "rule": "$title_en$.$season_episode$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Breaking.Bad.S02E01.2160p.WEB-DL.5xRus.Ukr.Eng.TrollUHD-ULTRAHDCLUB.mkv", + "rule": "$title_en$.$season_episode$.$resolution$.$source$.$extension$", + "replaces": [ + { + "src": "5xRus.Ukr.Eng.", + "target": "" + } + ] + }, + { + "example": "Big.Bet.S01E01.2022.Disney+.WEB-DL.4K.HEVC.HDR.DDP-HDCTV.mkv", + "rule": "$title_en$.$season_episode$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Hanzawa.Naoki.S01E01.2013.DC.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$season_episode$.$year$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Great.British.Railway.Journeys.S01E02.HDTV.720p.x264-CiA.mkv", + "rule": "$title_en$.$season_episode$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "Skam.S1E01.WEBRip.1080P.不着调字幕组.mkv", + "rule": "$title_en$.$season_episode$.$source$.$resolution$.$extension$" + }, + { + "example": "Skam.S1E01.你看起来像个荡女彐.中挪字幕.WEBRip.1080P.不着调字幕组.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_cn$.$source$.$resolution$.$extension$" + }, + { + "example": "Skam.S2E02.你对一个朋友撒谎却怪罪于我.SweSub.1080p.WEB-DL.H264.mp4", + "rule": "$title_en$.$season_episode$.$episode_title_cn$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Oh.No.Here.Comes.Trouble.S01E02.2023.1080p.WEB-DL.AAC.H264-JKCT.mkv", + "rule": "$title_en$.$season_episode$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Yes,Minister.S03E08.1984.Party.Games.480p.DVDrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$season_episode$.$year$.$episode_title_en$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Young.Sheldon.S05E01.One.Bad.Night.and.Chaos.of.Selfish.Desires.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$extension$" + }, + { + "example": "The.Universe.S01E01.Secrets.of.the.Sun.2007.BD-REMUX.1080P.H264.AC3-FrankB.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Universe.S01E01.Secrets.of.the.Sun.2007.BD-REMUX.1080P.H264.AC3-FrankB.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$year$.$source$.$resolution$.$video_codec$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Arrow.S01E01.Pilot.Bluray.1080p.x265.10bit.DDP.5.1.MNHD-FRDS.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Supernatural.S06E22.The.Man.Who.Knew.Too.Much.Blu-Ray.AC3.x265.10bit-Yumi.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "The.Universe.S05E01.7.Wonders.of.the.Solar.System.BD-REMUX.1080P.H264.AVC.DTS-HD-FrankB.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$source$.$resolution$.$video_codec$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Australia.with.Simon.Reeve.S01E01.WEB-DL.720p.H.264.AAC-FOXKING.mp4", + "rule": "$title_en$.$season_episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Universe.S09E01.Omens.of.Doom.1080p.x264.Eac3-FrankB.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Universe.S08E01.2014.stonehenge.1080p.BD-REMUX.H264.AVC.DTS-FrankB.mkv", + "rule": "$title_en$.$season_episode$.$year$.$episode_title_en$.$resolution$.$source$.$video_codec$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "SEAL.Team.S03E11E12.2019.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$season_episode_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Friends.S10E17E18.1080p.BluRay.Remux.AVC.AC3-WhaleHu.mkv", + "rule": "$title_en$.$season_episode_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Better Call Saul S01E01 2160p WEB-DL DTS x265-TrollUHD.mkv", + "rule": "$title_en$ $season_episode$ $resolution$ $source$ $audio_codec$ $video_codec$.$extension$" + }, + { + "example": "The Big Bang Theory S03E01 The Electric Can Opener Fluctuation 1080p BluRay Remux VC1 DD5.1-Gamma.mkv", + "rule": "$title_en$ $season_episode$ $episode_title_en$ $resolution$ $source$ $video_codec$ $audio_codec$.$extension$" + }, + { + "example": "The Sopranos S01E01 1080p Blu-ray Remux AVC DTS-HD MA 5.1.mkv", + "rule": "$title_en$ $season_episode$ $resolution$ $source$ $video_codec$ $audio_codec$.$extension$" + }, + { + "example": "My.Own.Swordsman.2006.S01E01.2160p.WEB-DL.H265.AAC-LeagueWEB.mp4", + "rule": "$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": " Immigration.Nation.2020.S01E01.Installing.Fear.1080p.WEB-DL.DDP5.1.x264.mkv", + "rule": "$title_en$.$year$.$season_episode$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "America.the.Beautiful.2022.S01E01.1080p.WEB.h264-KOGi.mkv", + "rule": "$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Explore.with.the.Note.2019.E01.1080p.WEB-DL.H264.AAC-LeagueWEB.mp4", + "rule": "$title_en$.$year$.$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Into.the.Universe.With.Stephen.Hawking.2010.E03.1080p.Bluray.x265.10bit.MNHD-FRDS.mkv", + "rule": "$title_en$.$year$.$episode$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Under.the.Microscope.2023.S01E01.WEB-DL.2160P.HEVC.AAC-CJWL.mp4", + "rule": "$title_en$.$year$.$season_episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Fringe.2008.S01E01.Blu-Ray.1080p.DD5.1.x265.10bit-Yumi.mkv", + "rule": "$title_en$.$year$.$season_episode$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "One.Dream.One.Home.2019.E01.WEB-DL.4K.HLG10.H265.AAC-HDCTV.mp4", + "rule": "$title_en$.$year$.$episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Eight.Hours.2022.E09.Repack.WEB-DL.4K.H265.DDP.AAC-HDCTV.mp4", + "rule": "$title_en$.$year$.$episode$.$episode_title_en$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Howl's.Moving.Castle.2004.BluRay.1080p.x265.10bit.4Audio.MNHD-FRDS.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Heidi.2015.BluRay.1080p.DTS-HD.MA.5.1.3Audio.x264-EPiC.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Love.is.Pyjamas.2012.Blu-ray.1080p.Remux.AVC.TrueHD.7.1-Dream.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Downton.Abbey.A.New.Era.2022.2160p.WEB-DL.DDP5.1.Atmos.HDR.DV.x265-HDFans.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$", + "replaces": [ + { + "src": ".MA.", + "target": "." + } + ] + }, + { + "example": "Triangle.of.Sadness.2022.CC.UHD.BluRay.2160p.x265.10bit.HDR.mUHD-FRDS.mkv", + "rule": "$title_en$.$year$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "Empresses.in.the.Palace.E01.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "rule": "$title_en$.$episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Last.of.Us.Game.CG.2160p.60fps.WEB-DL.H264.AAC-Stone.mkv", + "rule": "$title_en$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[cX]A.Small.Light.E08.2023.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$title_en$.$episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", + "replaces": [ + { + "src": "[cX]", + "target": "" + } + ] + }, + { + "example": "The.Bad.Kids.E01.2020.WEB-DL.4K.H265.AAC-Enichi.mp4", + "rule": "$title_en$.$episode$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "1899.2022.S01E01.1080p.NF.WEB-DL.H264.DDP5.1.Atmos-OurTV.mkv", + "rule": "$title_year$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Prince of Persia the Sands of Time 2010 BluRay 1080p x265 2Audio-WGXC@HDFans.mkv", + "rule": "$title_en$ $year$ $source$ $resolution$ $video_codec$ $audio_codec$.$extension$" + }, + { + "example": "[DMG] EDENS ZERO Menu Vol.1 [BDRip][HEVC_FLAC][1080P_Ma10P](4ABDCA70).mkv", + "rule": "$title_en$ [$source$][$video_codec$_$audio_codec$][$resolution$]($mix_numbers_letters$).$extension$", + "replaces": [ + { + "src": "Menu Vol.1 ", + "target": "" + } + ] + }, + { + "example": "[Isekai Ojisan][Vol.01][Menu][BDRIP][1080P][H264_FLAC].mkv", + "rule": "[$title_en$][$source$][$resolution$][$video_codec$_$audio_codec$].$extension$", + "replaces": [ + { + "src": "[Vol.01]", + "target": "" + }, + { + "src": "[Menu]", + "target": "" + } + ] + }, + { + "example": "[Isekai Ojisan][03][BDRIP][1080P][H264_FLAC].mkv", + "rule": "[$title_en$][$episode_number$][$source$][$resolution$][$video_codec$_$audio_codec$].$extension$" + }, + { + "example": "2.Broke.Girls.S01E01.2011.1080p.WEB-DL.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$number$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "2.Broke.Girls.S06E01E02.2016.1080p.WEB-DL.x265.10bit.AC3£cXcY@FRDS.mkv", + "rule": "$number$.$title_en$.$season_episode_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "风味人间.Once.Upon.A.Bite.2022.S04E01.2160p.50FPS.WEB-DL.H265.DV.DDP2.0-OurTV.mp4", + "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "斗罗大陆.Dou.Luo.Da.Lu.2018.S01E01.1080p.WEB-DL.AAC.H264-OurTV.mp", + "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "The.Vietnam.War.E01.2017.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "rule": "$title_en$.$episode$.$year$.$source$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "USA.A.West.Coast.Journey.2014.2160p.BluRay.x265.10bit.SDR.mUHD-FRDS.mkv", + "rule": "$title_en$.$year$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Alone.S09E11.Fight.Flight.or.Freeze.1080p.AMZN.WEB-DL.AAC2.0.H.264-SMURF.mkv", + "rule": "$title_en$.$season_episode$.$episode_title_en$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "BBC.Frozen.Planet.EP01.2011.1080p.BluRay.DTS.x264-HDS.mkv", + "rule": "$title_en$.$episode$.$year$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Blue Planet II 2017 D1 UHD BluRay REMUX 2160p HEVC DTS-HD MA5.1 2Audio-CHD.mkv", + "rule": "$title_en$ $year$ $source$ $resolution$ $video_codec$ $audio_codec$.$extension$" + }, + { + "example": "Our Planet 2019 S01E04 Coastal Seas 2160p NF WEBRip x265.mkv", + "rule": "$title_en$ $year$ $season_episode$ $episode_title_en$ $resolution$ $source$ $video_codec$.$extension$" + }, + { + "example": "CCTV.Bian.Jiang.Xing.E01.720p.HDTV.x264-NGB.mkv", + "rule": "$title_en$.$episode$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "CCTV.Bian.Jiang.Xing.E01.720p.HDTV.x264.AC3-NGB.mkv", + "rule": "$title_en$.$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Earth.Flight.Ep01.North.America.1080p.BluRay.x264-xiaofriend.mkv", + "rule": "$title_en$.$episode$.$episode_title_en$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "Earth.Flight.Ep01.North.America.1080p.BluRay.x264-xiaofriend.mkv", + "rule": "$title_en$.$episode$.$episode_title_en$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "How.Earth.Made.Us.EP01.HK.2010.BluRay.1080P.DTS-HD.MA2.0.x264-th71@beAst.mkv", + "rule": "$title_en$.$episode$.$year$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "早餐中国.Breakfast.In.China.S04E31.2020.2160p.WEB-DL.H265.mkv", + "rule": "$title_cn$.$title_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$extension$" + }, + { + "example": "风味人间.Once.Upon.a.Bite.2018.S01E01.WEB-DL.4K.VP9.AAC-PTerWEB.mkv", + "rule": "$title_cn$.$title_en$.$year$.$season_episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "[易中天品三国52千古风流].Yi.Zhong.Tian.Pin.San.Guo.E52.2006.DVDRip.576p.x264.AC3-CMCT.mkv", + "rule": "[$title_cn$$episode_number$$episode_title_cn$].$title_en$.$episode$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.S01E01.Standing.Up.in.the.Milky.Way.2014.BluRay.1080p.x265.DTS.5.1.mkv", + "rule": "$title_cn$.$title_en$.$season_episode$.$episode_title_en$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "The.Civil.War.E9.1865.The.Better.Angels.of.Our.Nature.720p.BluRay.DD5.1.x264-DON.mkv", + "rule": "$title_en$.$episode$.$year$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Planet.Earth.EP01.From.Polo.To.Polo.2006.BluRay.1080p.MultiAudio.DTS-HD.HR.5.1.x264-beAst.mkv", + "rule": "$title_en$.$episode$.$episode_title_en$.$year$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "BBC.Attenborough.60.Years.in.the.Wild.2012.EP01.BluRay.1080p.DTS-HD.MA.2.0.x265.10bit-BeiTai.mkv", + "rule": "$title_number_en$.$year$.$episode$.$source$.$resolution$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Around.The.World.In.80.Gardens.2008.E10.1080p.WEB-DL.H264.AAC-PTerWEB.mp4", + "rule": "$title_number_en$.$year$.$episode$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Around.The.World.In.80.Gardens.2008.E10.1080p.WEB-DL.H264.AAC-PTerWEB.mp4", + "rule": "$title_number_en$.$year$.$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Formula.1.Drive.to.Survive.S03E01.2021.Netflix.WEB-DL.4K.HEVC.HDR.DDP-HDCTV.mkv", + "rule": "$title_number_en$.$season_episode$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "John Wick Chapter 4 2023 UHD BluRay 2160p HEVC Atmos TrueHD7.1-BHYS@OurBits", + "rule": "$title_number_en$ $year$ $color$ $source$ $resolution$ $video_codec$ $audio_codec$" + }, + { + "example": "Attenborough 60 Years in the Wild 2012 2Disc Blu-ray 1080i AVC DTS-HD 2.0-TTG", + "rule": "$title_number_en$ $year$ $source$ $resolution$ $video_codec$ $audio_codec$" + }, + { + "example": "13.Reasons.Why.S01E01.Tape.1.Side.A.2160p.NF.WEB-DL.DDP5.1.HDR.HEVC-HHWEB.mkv", + "rule": "$title_number_en$.$season_episode$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Re.Zero.kara.Hajimeru.Isekai.Seikatsu.E26.Crunchyroll.WEB-DL.1080p.x264.AAC-HDCTV.mkv", + "rule": "$title_en$.$episode$.$episode_title_en$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Record.of.Ragnarok.II.2023.S02E01.1080p.NF.WEB-DL.DDP2.0.x264-PTerWEB.mkv", + "rule": "$title_en$.$year$.$season_episode$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Record.of.Ragnarok.II.2023.S02E01.1080p.NF.WEB-DL.x264.DDP2.0-PTerWEB.mkv", + "rule": "$title_en$.$year$.$season_episode$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Record.of.Ragnarok.II.2023.S02E01.1080p.NF.WEB-DL.DDP2.0.x264-PTerWEB.mkv", + "rule": "$title_en$.$year$.$season_episode$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Cyberpunk.Edgerunners.E02.Like.A.Boy.2160p.NF.Blue-ray.DDP5.1.H.264-SPADE.mkv", + "rule": "$title_en$.$episode$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "Cyberpunk.Edgerunners.02.Like.A.Boy.2160p.NF.Blue-ray.DDP5.1.H.264-SPADE.mkv", + "rule": "$title_en$.$episode_number$.$episode_title_en$.$resolution$.$source$.$audio_codec$.$video_codec$.$extension$" + }, + { + "example": "[VCB-Studio] OVERLORD [13][1080p][x265_flac].mkv", + "rule": "$title_en$ [$episode_number$][$resolution$][$video_codec$_$audio_codec$].$extension$" + }, + { + "example": "[VCB-Studio] Durarara!!×2 Shou [01][Ma10p_1080p][x265_flac_aac].mkv", + "rule": "$title_number_en$ [$episode_number$][$resolution$][$video_codec$_$audio_codec$].$extension$" + }, + { + "example": "Dragon.Ball.150.480p.x264.mkv", + "rule": "$title_en$.$episode_number$.$resolution$.$video_codec$.$extension$" + }, + { + "example": "Night on Earth S01E01 Moonlit Plains 2160p WEBRip DD+5.1 HDR x265-Chotab.mkv", + "rule": "$title_en$ $season_episode$ $episode_title_en$ $resolution$ $source$ $audio_codec$ $video_codec$.$extension$" + }, + { + "example": "[sergey_krs] Kuroshitsuji II - 01 [BDRip 1920x1080 x264 FLAC].mkv", + "rule": "$title_en$ $episode_number$ [$source$ $resolution$ $video_codec$ $audio_codec$].$extension$" + }, + { + "example": "[sergey_krs] Kuroshitsuji II - 01 [BDRip 1920x1080 x264 FLAC].mkv", + "rule": "$title_en$ $episode_number$ [$source$ $resolution$ $audio_codec$ $video_codec$].$extension$" + }, + { + "example": "[sergey_krs] Kuroshitsuji II - 01 [BDRip 1920x1080 x264 FLAC].mkv", + "rule": "[$title_en$][$episode_number$][$source$][$audio_codec$][$resolution$].$extension$" + }, + { + "example": "[Nekomoe kissaten&VCB-Studio] Mushoku Tensei ~Isekai Ittara Honki Dasu~ [01][Ma10p_1080p][x265_flac].mkv", + "rule": "$title_en$ [$episode_number$][$resolution$][$video_codec$_$audio_codec$].$extension$" + }, + { + "example": "[NC-Raws] OVERLORD IV - 01 (B-Global 3840x2160 HEVC AAC MKV) [038B942D].mkv", + "rule": "$title_en$ $episode_number$ $resolution$ $video_codec$ $audio_codec$ [$mix_numbers_letters$].$extension$", + "replaces": [ + { + "src": "B-Global ", + "target": "" + }, + { + "src": " MKV", + "target": "" + }, + { + "src": " (", + "target": " " + }, + { + "src": ") ", + "target": " " + } + ] + }, + { + "example": "[DMG] EDENS ZERO 第01话「桜舞うソラに」 [BDRip][HEVC_FLAC][1080P_Ma10P](3E87A76C).mkv", + "rule": "$title_en$ $episode_cn$ $episode_title_jp$ [$source$][$video_codec$_$audio_codec$][$resolution$]($mix_numbers_letters$).$extension$", + "replaces": [ + { + "src": "「", + "target": " " + }, + { + "src": "」 ", + "target": " " + } + ] + }, + { + "example": "Mobseka 2022 S01E01-[1080p][BDRIP][x265.FLAC].mkv", + "rule": "$title_en$ $year$ $season_episode$-[$resolution$][$source$][$video_codec$.$audio_codec$].$extension$" + }, + { + "example": "Onipan! S01E01-[1080p][BDRIP][x265.FLAC].mkv", + "rule": "$title_en$ $season_episode$-[$resolution$][$source$][$video_codec$.$audio_codec$].$extension$" + }, + { + "example": "Zom.100.Bucket.List.of.the.Dead.S01E01.2023.2160p.WEB-DL.H264.AAC.mkv", + "rule": "$title_number_en$.$season_episode$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$" + }, + { + "example": "Hunter X Hunter 2011 - EP001 [BD 1920x1080 23.976fps AVC-yuv444p10 FLAC Chap] v2 - mawen1250.mkv", + "rule": "$title_en$ $year$ $episode$ [$anything$].$extension$" + }, + { + "example": "圣斗士星矢第52话:亚历士,传说中的魔皇拳.mkv", + "rule": "$title_cn$ $episode_cn$ $episode_title_cn$.$extension$", + "regex_replaces": [ + { + "src": "$episode_cn$", + "target": " $episode_cn$ " + } + ], + "replaces": [ + { + "src": ":", + "target": "" + } + ] + }, + { + "example": "Air.Emergency.S16E01.Deadly.Silence.死寂(1999年里尔35公务机空难).mp4", + "rule": "$title_en$.$season_episode$.$anything$.$extension$" + } + ] +} \ No newline at end of file diff --git a/soda_resource_tools_lib/src/soda.rs b/soda_resource_tools_lib/src/soda.rs index 5e1de1a2..d833b1e7 100644 --- a/soda_resource_tools_lib/src/soda.rs +++ b/soda_resource_tools_lib/src/soda.rs @@ -1,11 +1,17 @@ -use std::sync::Mutex; +use std::{ + collections::HashMap, + fs, + path::{self, Path}, + sync::Mutex, +}; use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; -use crate::soda::entity::{MTInfo, MTMetadata, TransferType}; +use crate::soda::entity::{MTInfo, MTMetadata, RenameStyle, TransferType}; use self::{ - entity::{LibConfig, ResourceType, ScrapeConfig}, + entity::{LibConfig, MetaContext, NamesMap, ResourceType, ScrapeConfig, SodaError}, meta::strong_match_token, }; @@ -18,7 +24,7 @@ pub(crate) mod fanart; pub(crate) mod filebrowser; pub(crate) mod finder; pub(crate) mod global; -pub(crate) mod meta; +pub mod meta; pub(crate) mod request; pub(crate) mod scraper; pub(crate) mod tmdb; @@ -26,11 +32,7 @@ pub(crate) mod transfer; pub(crate) mod utils; pub(crate) mod watcher; -pub(crate) static LIB_CONFIG: Lazy> = Lazy::new(|| { - let config = LibConfig::new(); - tracing::info!("LIB_CONFIG = {:?}", config); - Mutex::new(config) -}); +pub(crate) static LIB_CONFIG: Lazy> = Lazy::new(|| Mutex::new(LibConfig::new())); pub fn get_lib_config() -> LibConfig { let config = LIB_CONFIG.lock().unwrap(); @@ -40,7 +42,6 @@ pub fn get_lib_config() -> LibConfig { /// 初始化配置 pub fn init_lib_config() { let mut config = LIB_CONFIG.lock().unwrap(); - strong_match_token::init(); } @@ -50,10 +51,6 @@ pub fn update_lib_config(new_config: LibConfig) { config.update(new_config); } -pub fn create_mt_metadata(title: &str) -> Option { - return meta::mt_metadata(title); -} - /// 刮削源目录资源到目标目录。 /// /// 刮削要分成多个步骤 @@ -62,56 +59,284 @@ pub fn create_mt_metadata(title: &str) -> Option { /// 3. 远程识别要整理的资源 /// 4. 转移文件 /// 5. 刮削要整理的资源 -pub fn scrape(resource_type: ResourceType, transfer_type: TransferType, scrape_config: ScrapeConfig, src_directory: String, target_directory: String) { - init_lib_config(); +/// +/// https://post.smzdm.com/p/aox8wp36/ +/// https://emby.media/support/articles/Movie-Naming.html +/// https://support.emby.media/support/solutions/articles/44001159110-tv-naming +/// +pub fn scrape( + resource_type: ResourceType, + transfer_type: TransferType, + scrape_config: ScrapeConfig, + src_directory: String, + target_directory: String, +) { + tracing::debug!( + "scrape_src_to_target resource_type {:?}, transfer_mode {:?}, src_directory {:?}, target_directory {:?}", + resource_type, + transfer_type, + src_directory, + target_directory + ); + + let mut paths: Vec = Vec::new(); + + // 找到资源 + let mut callback = |path: String| { + tracing::debug!("find path = {}", path); + paths.push(path); + }; + finder::find(&resource_type, &src_directory, &mut callback); - tracing::info!("scrape_src_to_target resource_type {:?}, transfer_mode {:?}, src_directory {:?}, target_directory {:?}", resource_type, transfer_type, src_directory, target_directory); + tracing::info!(target:"soda::info", "资源数量: {}", paths.len()); - // 找到要整理的资源 - finder::find(&resource_type, &src_directory, |path: String| { - tracing::info!("find resource path = {:?}", path); + // paths按照父路径分组 + let mut paths_parent_group = HashMap::new(); + for ele in &paths { + let parent_path = Path::new(&ele).parent().unwrap().to_str().unwrap().to_string(); + paths_parent_group.insert(parent_path, ele); + } + tracing::info!(target:"soda::info", "资源组数量: {}", paths_parent_group.len()); + + // 缓存信息 + let mut mt_infos: HashMap = HashMap::new(); + + // 上下文 + let mut meta_context = MetaContext::new(); + + // 识别资源 + for src_path in paths { match resource_type { ResourceType::MT => { - // 本地识别要整理的资源 - if let Some(mut mt_meta) = meta::mt_metadata(&path) { - tracing::info!("meta mt_meta {:?}", mt_meta); - - let mut mt_info: Option = if scrape_config.enable_recognize { - // 远程识别要整理的资源 - if let Some(mut info) = tmdb::recognize_mt(&mut mt_meta) { - tracing::info!("recognize mt_info {:?}", info.original_title()); - Some(info) - } else { - tracing::error!("remote recognize resource failed, mt_meta {:?}", mt_meta); - None + // 初始化上下文 + if meta_context.init(&src_path) { + // 刮削 + match scrape_mt( + &mut meta_context, + &mut mt_infos, + src_path, + &scrape_config, + &target_directory, + &transfer_type, + ) { + Ok(_) => {} + Err(e) => { + tracing::error!(target: "soda::info","刮削失败 e = {}", e); + tracing::error!("recognize_scrape_mt failed e = {}", e); + // scrape_context.error = true; } - } else { - tracing::info!("remote recognize resource disabled, mt_meta {:?}", mt_meta); - None }; + } + } + } + } +} - // 更新媒体图片 - if let Some(mt_info) = &mut mt_info { - if scrape_config.enable_scrape_image { - fanart::obtain_images(mt_info); - } - } +fn scrape_mt( + meta_context: &mut MetaContext, + mt_infos: &mut HashMap, + src_path: String, + scrape_config: &ScrapeConfig, + target_directory: &String, + transfer_type: &TransferType, +) -> Result<(), SodaError> { + tracing::info!(target:"soda::info", "开始刮削: {}", src_path); - // 选择重命名格式 - let rename_format = if mt_meta.is_movie() { LIB_CONFIG.lock().unwrap().transfer_rename_format_movie.clone() } else { LIB_CONFIG.lock().unwrap().transfer_rename_format_tv.clone() }; + // 创建meta + let mut mt_meta = meta::create_metadata_mt(meta_context)?; - tracing::info!("rename_format {:?}", rename_format); + // 识别成功但是没有关键信息,那么取父目录的信息重新识别一次 + if mt_meta.title_cn.is_empty() && mt_meta.title_en.is_empty() { + let root_src_path = Path::new(&src_path).parent().unwrap().parent().unwrap(); + let root_path = root_src_path.to_str().unwrap().to_string(); + meta_context.init(&root_path); - // 转移文件 - if let Some(transferred_path) = transfer::transfer(&target_directory, &transfer_type, &rename_format, &mt_meta, &path) { - if let Some(mt_info) = &mut mt_info { - // 刮削要整理的资源 - scraper::scrape_metadata(&scrape_config, &mt_meta, &mt_info, &transferred_path); - } - }; + let mut root_mt_meta = meta::create_metadata_mt(meta_context)?; + + if root_mt_meta.title_cn.is_empty() && root_mt_meta.title_en.is_empty() { + return Err(SodaError::Str("title_cn and title_en is empty")); + } + + mt_meta.title_cn = root_mt_meta.title_cn; + mt_meta.title_en = root_mt_meta.title_en; + } + + // 如果是电视剧,但是缺少季信息,那么默认为第一季 + if !mt_meta.episode.is_empty() && mt_meta.season.is_empty() { + mt_meta.season = format!("S{:02}", 1); + tracing::debug!("set season = {:?}", mt_meta.season); + } + + name_mapping(&mut mt_meta); + + tracing::info!(target:"soda::info", "资源名解析成功: {}", serde_json::to_string(&mt_meta)?); + + if !scrape_config.enable_recognize { + return Err(SodaError::Str("enable_recognize is false")); + }; + + let mut final_target_path = String::new(); + + // 远程识别要整理的资源 + match tmdb::recognize_mt(mt_infos, &mut mt_meta) { + Ok(mt_info_key) => { + // 远程识别要整理的资源 + + let mut mt_info = mt_infos.get_mut(&mt_info_key).ok_or(SodaError::Str("mt_info is empty"))?; + + tracing::info!(target:"soda::info", "tmdb_id = {}, tvdb_id = {}, imdb_id = {}, title = {}",mt_info.tmdb_id(),mt_info.tvdb_id().unwrap_or("0".to_string()),mt_info.imdb_id().unwrap_or("0"), mt_info.title()); + + // 更新媒体图片 + if scrape_config.enable_scrape_image { + fanart::obtain_images(&mut mt_info); + } + + // 选择重命名格式 + let rename_style = LIB_CONFIG.lock().unwrap().rename_style.clone(); + + // 选择重命名格式 + let rename_format = if mt_meta.is_movie() { + LIB_CONFIG.lock().unwrap().transfer_rename_format_movie.clone() + } else { + LIB_CONFIG.lock().unwrap().transfer_rename_format_tv.clone() + }; + + // 生成转移文件路径 + let transfer_target_path = transfer::gen_mt_transfer_target_path(&target_directory, rename_style, &rename_format, &mt_meta); + + final_target_path = transfer_target_path.clone(); + + // 转移文件 + transfer::transfer_mt_file(&mt_meta, &src_path, &transfer_target_path, &transfer_type)?; + + // 刮削信息 + if scrape_config.enable_scrape_write { + scraper::scrape_metadata(scrape_config, &mt_meta, mt_info, &transfer_target_path); + } + } + Err(error) => { + tracing::error!(target: "soda::info","识别失败 e = {}", error); + + // 选择重命名格式 + let rename_style = LIB_CONFIG.lock().unwrap().rename_style.clone(); + + // 选择重命名格式 + let rename_format_str = if mt_meta.is_movie() { + LIB_CONFIG.lock().unwrap().transfer_rename_format_movie.clone() + } else { + LIB_CONFIG.lock().unwrap().transfer_rename_format_tv.clone() + }; + + // 选择重命名格式 + let rename_format = if mt_meta.is_movie() { + LIB_CONFIG.lock().unwrap().transfer_rename_format_movie.clone() + } else { + LIB_CONFIG.lock().unwrap().transfer_rename_format_tv.clone() + }; + + // 生成转移文件路径 + let transfer_target_path = transfer::gen_mt_transfer_target_path(&target_directory, rename_style, &rename_format, &mt_meta); + + final_target_path = transfer_target_path.clone(); + + // 转移文件 + transfer::transfer_mt_file(&mt_meta, &src_path, &transfer_target_path, &transfer_type)?; + } + } + + tracing::debug!("scrape_mt success mt_meta = {:?}", mt_meta); + + tracing::info!(target:"soda::info", "刮削结束: src={} target={}", src_path, final_target_path); + + return Ok(()); +} + +/// 处理名字映射,有些中文名或者英文名不准确,需要映射 +fn name_mapping(mt_meta: &mut MTMetadata) { + // 处理名字映射 + for name in &NAMES_MAP.names { + tracing::debug!("name_mapping name = {}", serde_json::to_string(&name).unwrap()); + tracing::debug!( + "name_mapping mt_meta.title_en = {:?} mt_meta.title_cn = {:?} mt.meta.year = {:?}", + mt_meta.title_en, + mt_meta.title_en, + mt_meta.year + ); + // + if !name.src.release_year.is_empty() && name.src.release_year == mt_meta.year { + if !name.src.title_cn.is_empty() && name.src.title_cn == mt_meta.title_cn { + if !name.target.title_cn.is_empty() { + mt_meta.title_cn = name.target.title_cn.clone(); + tracing::debug!( + "name_mapping 1 name.src.title_cn = {}, mt_meta.title_cn = {}", + name.src.title_cn, + mt_meta.title_cn + ); + } + if !name.target.title_en.is_empty() { + mt_meta.title_en = name.target.title_en.clone(); + tracing::debug!( + "name_mapping 2 name.src.title_en = {}, mt_meta.title_en = {}", + name.src.title_en, + mt_meta.title_en + ); + } + } + + // + if !name.src.title_en.is_empty() && name.src.title_en == mt_meta.title_en { + if !name.target.title_cn.is_empty() { + mt_meta.title_cn = name.target.title_cn.clone(); + tracing::debug!( + "name_mapping 1 name.src.title_cn = {}, mt_meta.title_cn = {}", + name.src.title_cn, + mt_meta.title_cn + ); + } + if !name.target.title_en.is_empty() { + mt_meta.title_en = name.target.title_en.clone(); + tracing::debug!( + "name_mapping 2 name.src.title_en = {}, mt_meta.title_en = {}", + name.src.title_en, + mt_meta.title_en + ); } } + } else { + // 映射英文名 + if !name.src.title_en.is_empty() && name.src.title_en == mt_meta.title_en && !name.target.title_en.is_empty() { + mt_meta.title_en = name.target.title_en.clone(); + tracing::debug!( + "name_mapping 3 name.src.title_en = {}, mt_meta.title_en = {}", + name.src.title_en, + mt_meta.title_en + ); + } + + // 映射中文名 + if !name.src.title_cn.is_empty() && name.src.title_cn == mt_meta.title_cn && !name.target.title_cn.is_empty() { + mt_meta.title_cn = name.target.title_cn.clone(); + tracing::debug!( + "name_mapping 4 name.src.title_cn = {}, mt_meta.title_cn = {}", + name.src.title_cn, + mt_meta.title_cn + ); + } } - }); + } } + +static NAMES_MAP: Lazy = Lazy::new(|| { + let config = LIB_CONFIG.lock().unwrap(); + + let content = if !config.strong_match_name_map.is_empty() { + config.strong_match_name_map.clone() + } else if !config.strong_match_name_map_path.is_empty() { + fs::read_to_string(config.strong_match_name_map_path.as_str()).unwrap() + } else { + unreachable!("strong_match_name_map and strong_match_name_map_path is empty") + }; + + serde_json::from_str(&content).unwrap() +}); diff --git a/soda_resource_tools_lib/src/soda/cache.rs b/soda_resource_tools_lib/src/soda/cache.rs index ca694391..e1048b16 100644 --- a/soda_resource_tools_lib/src/soda/cache.rs +++ b/soda_resource_tools_lib/src/soda/cache.rs @@ -1,90 +1,103 @@ use bytes::Bytes; +use once_cell::sync::Lazy; +use sled::Db; use crate::soda::utils::{self}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::thread; pub enum CacheType { TMDB, FANART, } -pub(crate) fn cache_remove(cache_type: &CacheType, key: &str) { - sled_cache_remove(cache_path(cache_type), key) -} +pub(crate) static TMDB_CACHE: Lazy> = Lazy::new(|| Mutex::new(sled::open(cache_path(&CacheType::TMDB)).expect("open"))); -fn cache_path(cache_type: &CacheType) -> String { - match cache_type { - CacheType::TMDB => utils::get_tmdb_http_cache_path(), - CacheType::FANART => utils::get_fanart_http_cache_path(), - } +pub(crate) static FANART_CACHE: Lazy> = Lazy::new(|| Mutex::new(sled::open(cache_path(&CacheType::FANART)).expect("open"))); + +pub(crate) fn cache_remove(cache_type: &CacheType, key: &str) { + let cache_db = cache_db(cache_type); + sled_cache_remove(cache_db, key) } pub(crate) fn cache_get(cache_type: &CacheType, key: &str) -> Option { - sled_ache_get(cache_path(cache_type), key) + let cache_db = cache_db(cache_type); + sled_cache_get(cache_db, key) } pub(crate) fn cache_save_bytes(cache_type: &CacheType, key: &str, value: &Bytes) { - sled_cache_save_bytes(cache_path(cache_type), key, value) + let cache_db = cache_db(cache_type); + sled_cache_save_bytes(cache_db, key, value) } pub(crate) fn cache_get_bytes(cache_type: &CacheType, key: &str) -> Option { - sled_cache_get_bytes(cache_path(cache_type), key) + let cache_db = cache_db(cache_type); + sled_cache_get_bytes(cache_db, key) +} + +fn cache_db(cache_type: &CacheType) -> MutexGuard<'_, Db> { + match cache_type { + CacheType::TMDB => TMDB_CACHE.lock().unwrap(), + CacheType::FANART => FANART_CACHE.lock().unwrap(), + } +} + +fn cache_path(cache_type: &CacheType) -> String { + match cache_type { + CacheType::TMDB => utils::get_tmdb_http_cache_path(), + CacheType::FANART => utils::get_fanart_http_cache_path(), + } } -fn sled_cache_remove(cache_path: String, url: &str) { - let tree = sled::open(cache_path).expect("open"); - tree.remove(url).expect("remove"); - tree.flush().expect("flush error"); - tracing::info!("cache remove key = {:?}", url); +fn sled_cache_remove(db: MutexGuard<'_, Db>, url: &str) { + db.remove(url).expect("remove"); + db.flush().expect("flush error"); + tracing::debug!("cache remove success key = {:?}", url); } -fn sled_cache_get_bytes(cache_path: String, key: &str) -> Option { - let tree = sled::open(cache_path).expect("open"); +fn sled_cache_get_bytes(db: MutexGuard<'_, Db>, key: &str) -> Option { let key = key; - match tree.get(key) { + match db.get(key) { Ok(value) => match value { Some(value) => { let value = value.as_ref(); let value = bytes::Bytes::from(value.to_vec()); - tracing::info!("cache hit key = {:?}", key); + tracing::debug!("hit cache success key = {:?}", key); return Some(value); } None => { - tracing::info!("cache not hit key = {}", key); return None; } }, Err(e) => { - tracing::info!("cache error key = {}", key); + tracing::error!("cache error key = {}", key); return None; } } } -fn sled_cache_save_bytes(cache_path: String, key: &str, value: &Bytes) { - let tree = sled::open(cache_path).expect("open"); +fn sled_cache_save_bytes(db: MutexGuard<'_, Db>, key: &str, value: &Bytes) { let key = key; - tree.insert(key, value.to_vec()).expect("insert"); - tree.flush().expect("flush error"); - tracing::info!("cache success key = {:?}", key); + db.insert(key, value.to_vec()).expect("insert"); + db.flush().expect("flush error"); + tracing::debug!("cache success key = {:?}", key); } -fn sled_ache_get(cache_path: String, key: &str) -> Option { - let tree = sled::open(cache_path).expect("open"); - match tree.get(key) { +fn sled_cache_get(db: MutexGuard<'_, Db>, key: &str) -> Option { + match db.get(key) { Ok(value) => match value { Some(value) => { let value = value.as_ref(); let value = String::from_utf8(value.to_vec()).unwrap(); - tracing::info!("cache hit key = {:?}", key); + tracing::debug!("hit cache success key = {:?}", key); return Some(value); } None => { - tracing::info!("cache not hit key = {}", key); return None; } }, Err(e) => { - tracing::info!("cache error key = {}", key); + tracing::error!("cache error key = {}", key); return None; } } diff --git a/soda_resource_tools_lib/src/soda/entity.rs b/soda_resource_tools_lib/src/soda/entity.rs index 07751532..e43d5516 100644 --- a/soda_resource_tools_lib/src/soda/entity.rs +++ b/soda_resource_tools_lib/src/soda/entity.rs @@ -1,12 +1,14 @@ use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; +use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::span::{self, Id}; use crate::soda::extension_option::OptionExtensions; -use super::tmdb::entity::{TmdbCast, TmdbCrew, TmdbEpisode, TmdbGenre, TmdbSeason, TmdbSeasonInfo, TmdbTV, TmdbTVInfo}; +use super::tmdb::entity::{TmdbCast, TmdbCrew, TmdbEpisode, TmdbGenre, TmdbMovie, TmdbMovieInfo, TmdbSeason, TmdbSeasonInfo, TmdbTV, TmdbTVInfo}; use std::error::Error; use std::fmt::{self, Display, Formatter}; @@ -69,53 +71,289 @@ impl From for SodaError { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RenameStyle { + /// Emby 重命名格式 + Emby, +} + +impl std::fmt::Display for RenameStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + RenameStyle::Emby => "emby", + }; + s.fmt(f) + } +} + +impl std::str::FromStr for RenameStyle { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "emby" => Ok(Self::Emby), + _ => Err(format!("Unknown type: {s}")), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EmbyRenameStyle { + /// 影视 - 电影 重命名格式 + /// ```text + /// https://emby.media/support/articles/Movie-Naming.html + /// + /// 按照如下格式和顺序重命名 + /// + /// /300 (2006)/300 (2006) - 1080p.mkv + /// /300 (2006)/300 (2006).mkv + /// /300/300.mkv + /// ``` + EmbyMovie, + + /// 影视 - 电视剧 重命名格式 + /// ```text + /// https://emby.media/support/articles/TV-Naming.html + /// + /// 按照如下格式和顺序重命名 + /// + /// \Glee (2009)\Season 1\S01E01.mp4 + /// \Glee\Season 1\S01E01.mp4 + /// + /// ``` + EmbyTV, +} +impl EmbyRenameStyle { + pub(crate) fn rename(&self, mt_meta: &MTMetadata) -> PathBuf { + tracing::debug!("emby rename style = {:?}", self); + + match &self { + EmbyRenameStyle::EmbyMovie => { + let title = if !mt_meta.title_cn.is_empty() { + mt_meta.title_cn.clone() + } else { + mt_meta.title_en.clone() + }; + + if !title.is_empty() && !mt_meta.year.is_empty() && !mt_meta.resolution.is_empty() && !mt_meta.extension.is_empty() { + // /300 (2006)/300 (2006) - 1080p.mkv + + let path = PathBuf::new() + .join(format!("{} ({})", title, mt_meta.year)) + .join(format!("{} ({}) - {}.{}", title, mt_meta.year, mt_meta.resolution, mt_meta.extension)); + + tracing::debug!("emby EmbyMovie style = {:?}", path.to_str().unwrap()); + + return path; + } + + if !title.is_empty() && !mt_meta.year.is_empty() && !mt_meta.extension.is_empty() { + // /300 (2006)/300 (2006).mkv + + let path = PathBuf::new() + .join(format!("{} ({})", title, mt_meta.year)) + .join(format!("{} ({}).{}", title, mt_meta.year, mt_meta.extension)); + + tracing::debug!("emby EmbyMovie style = {:?}", path.to_str().unwrap()); + + return path; + } + + if !title.is_empty() && !mt_meta.extension.is_empty() { + // /300/300.mkv + + let path = PathBuf::new().join(format!("{}", title)).join(format!("{}.{}", title, mt_meta.extension)); + + tracing::debug!("emby EmbyMovie style = {:?}", path.to_str().unwrap()); + + return path; + } + + return unreachable!("emby movie rename not implement"); + } + EmbyRenameStyle::EmbyTV => { + let title = if !mt_meta.title_cn.is_empty() { + mt_meta.title_cn.clone() + } else { + mt_meta.title_en.clone() + }; + + // \Glee (2009)\Season 1\S01E01.mp4 + if !title.is_empty() + && !mt_meta.year.is_empty() + && !mt_meta.season.is_empty() + && !mt_meta.episode.is_empty() + && !mt_meta.extension.is_empty() + { + let path = PathBuf::new() + .join(format!("{} ({})", title, mt_meta.year)) + .join(format!("Season {}", mt_meta.season_number().unwrap_or(1))) + .join(format!( + "S{:02}E{:02}.{}", + mt_meta.season_number().unwrap_or(1), + mt_meta.episode_number().unwrap_or(1), + mt_meta.extension + )); + + tracing::debug!("emby EmbyTV style = {:?}", path.to_str().unwrap()); + + return path; + } + + // \Glee\Season 1\S01E01.mp4 + if !title.is_empty() + && mt_meta.year.is_empty() + && (mt_meta.season.is_empty() || !mt_meta.season.is_empty()) + && !mt_meta.episode.is_empty() + && !mt_meta.extension.is_empty() + { + let path = PathBuf::new() + .join(format!("{}", title)) + .join(format!("Season {}", mt_meta.season_number().unwrap_or(1))) + .join(format!( + "S{:02}E{:02}.{}", + mt_meta.season_number().unwrap_or(1), + mt_meta.episode_number().unwrap_or(1), + mt_meta.extension + )); + + tracing::debug!("emby EmbyTV style = {:?}", path.to_str().unwrap()); + + return path; + } + + return unreachable!("emby tv rename not implement"); + } + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LibConfig { /// 缓存路径 pub cache_path: String, - /// 强匹配规则路径 - pub strong_match_rules_path: String, + + /// 剧集 强匹配规则路径 + pub strong_match_rules_tv_path: String, + pub strong_match_rules_tv: String, + + /// 电影 强匹配规则路径 + pub strong_match_rules_movie_path: String, + pub strong_match_rules_movie: String, + /// 强匹配正则规则路径 pub strong_match_regex_rules_path: String, - /// 是否启用强匹配缓存 - pub strong_match_regex_enable_cache: bool, + pub strong_match_regex_rules: String, + + /// 强匹配名称映射路径 + pub strong_match_name_map_path: String, + pub strong_match_name_map: String, + /// 是否跳过特典 pub metadata_skip_special: bool, + /// 影视 - 电视剧 重命名格式 + /// ```text + /// https://emby.media/support/articles/TV-Naming.html + /// + /// \TV + /// \Glee (2009) + /// \Season 1 + /// Glee S01E01.mp4 + /// Glee S01E02.mp4 + /// \TV + /// \Seinfeld (1989) + /// Seinfeld S01E01.mp4 + /// Seinfeld S01E02.mp4 + /// ``` pub transfer_rename_format_tv: String, + /// 影视 - 电影 重命名格式 + /// ```text + /// https://emby.media/support/articles/Movie-Naming.html + /// + /// \Movies\Avatar (2009)\Avatar (2009).mkv + /// \Movies\Pulp Fiction (1994)\Pulp Fiction (1994).mp4 + /// \Movies\Reservoir Dogs (1992)\Reservoir Dogs (1992).mp4 + /// \Movies\The Usual Suspects (1995)\The Usual Suspects (1995).mkv + /// \Movies\Top Gun (1986)\Top Gun (1986).mp4 + /// /Movies + /// /300 (2006) + /// /300 (2006)/300 (2006) - 1080p.mkv + /// /300 (2006)/300 (2006) - 4K.mkv + /// /300 (2006)/300 (2006) - 720p.mp4 + /// /300 (2006)/300 (2006) - extended edition.mp4 + /// /300 (2006)/300 (2006) - directors cut.mp4 + /// /300 (2006)/300 (2006) - 3D.hsbs.mp4 + /// ``` pub transfer_rename_format_movie: String, + + /// 电影和电视剧重命名格式 + pub rename_style: Option, } impl LibConfig { pub fn update(&mut self, config: LibConfig) { - tracing::info!("update config = {:?}", config); self.cache_path = config.cache_path; - self.strong_match_rules_path = config.strong_match_rules_path; + + // self.strong_match_regex_rules_path = config.strong_match_regex_rules_path; - self.strong_match_regex_enable_cache = config.strong_match_regex_enable_cache; - self.metadata_skip_special = config.metadata_skip_special; + self.strong_match_regex_rules = config.strong_match_regex_rules; + + // + self.strong_match_rules_tv_path = config.strong_match_rules_tv_path; + self.strong_match_rules_tv = config.strong_match_rules_tv; + + // + self.strong_match_rules_movie_path = config.strong_match_rules_movie_path; + self.strong_match_rules_movie = config.strong_match_rules_movie; + + // + self.strong_match_name_map_path = config.strong_match_name_map_path; + self.strong_match_name_map = config.strong_match_name_map; + + // self.transfer_rename_format_tv = config.transfer_rename_format_tv; self.transfer_rename_format_movie = config.transfer_rename_format_movie; + + // + self.metadata_skip_special = config.metadata_skip_special; + + // + self.rename_style = config.rename_style; } pub fn new() -> LibConfig { let current_path = std::env::current_dir().unwrap(); return LibConfig { + // cache_path: current_path.join("cache").to_str().unwrap().to_string(), - strong_match_rules_path: current_path.join("config").join("mt_strong_match_rules.json").to_str().unwrap().to_string(), + // + strong_match_rules_tv_path: current_path.join("config").join("mt_strong_match_rules_tv.json").to_str().unwrap().to_string(), + // + strong_match_rules_movie_path: current_path.join("config").join("mt_strong_match_rules_movie.json").to_str().unwrap().to_string(), + // strong_match_regex_rules_path: current_path.join("config").join("mt_strong_match_regex_rules.json").to_str().unwrap().to_string(), + // + strong_match_name_map_path: current_path.join("config").join("mt_strong_match_name_map.json").to_str().unwrap().to_string(), + // transfer_rename_format_tv: "$title_cn$.$title_en$.$release_year$/$title_cn$.$title_en$.$year$.$season$.$resolution$.$source$.$video_codec$.$audio_codec$/$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$".to_string(), - transfer_rename_format_movie: "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$".to_string(), - strong_match_regex_enable_cache: false, + transfer_rename_format_movie: "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$/$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$".to_string(), metadata_skip_special: false, + strong_match_rules_tv: "".to_string(), + strong_match_rules_movie: "".to_string(), + strong_match_regex_rules: "".to_string(), + strong_match_name_map: "".to_string(), + rename_style: None, }; } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScrapeConfig { - /// 是否刮削图片 + /// 是否将刮削信息写入文件,nfo、image + pub enable_scrape_write: bool, + /// 是否刮削图片,从网络获取图片 pub enable_scrape_image: bool, /// 是否识别媒体资源 pub enable_recognize: bool, @@ -123,7 +361,11 @@ pub struct ScrapeConfig { impl ScrapeConfig { pub fn new() -> ScrapeConfig { - return ScrapeConfig { enable_scrape_image: true, enable_recognize: true }; + return ScrapeConfig { + enable_scrape_image: true, + enable_recognize: true, + enable_scrape_write: true, + }; } } @@ -134,14 +376,18 @@ pub enum MTInfo { } impl MTInfo { - pub(crate) fn new(tmdb_tv: TmdbTV) -> MTInfo { + pub(crate) fn new_movie(tmdb_movie: TmdbMovie) -> MTInfo { + MTInfo::MOVIE(MovieType::TMDB(TmdbMovieInfo::new(tmdb_movie))) + } + + pub(crate) fn new_tv(tmdb_tv: TmdbTV) -> MTInfo { MTInfo::TV(TVType::TMDB(TmdbTVInfo::new(tmdb_tv))) } pub(crate) fn title(&self) -> &str { match self { MTInfo::MOVIE(movie) => match movie { - MovieType::TMDB() => "", + MovieType::TMDB(movie) => movie.movie.name(), }, MTInfo::TV(tv) => match tv { TVType::TMDB(tv) => tv.tv.name(), @@ -152,7 +398,7 @@ impl MTInfo { pub(crate) fn original_title(&self) -> &str { match self { MTInfo::MOVIE(movie) => match movie { - MovieType::TMDB() => "", + MovieType::TMDB(movie) => "", }, MTInfo::TV(tv) => match tv { TVType::TMDB(tv) => tv.tv.original_name(), @@ -164,7 +410,7 @@ impl MTInfo { pub(crate) fn tmdb_id(&self) -> i64 { match self { MTInfo::MOVIE(movie) => match movie { - MovieType::TMDB() => -1, + MovieType::TMDB(movie) => movie.movie.id.clone(), }, MTInfo::TV(tv) => match tv { TVType::TMDB(tv) => tv.tv.id.clone(), @@ -172,31 +418,31 @@ impl MTInfo { } } - pub(crate) fn tvdb_id(&self) -> Option { + pub(crate) fn tvdb_id(&self) -> Option { match self { MTInfo::TV(tv) => match tv { TVType::TMDB(tv) => { - if let Some(external_ids) = &tv.tv.external_ids { - return Some(external_ids.tvdb_id()); - } - return None; + return tv.tv.tvdb_id(); + } + }, + MTInfo::MOVIE(movie) => match movie { + MovieType::TMDB(movie) => { + return movie.movie.tvdb_id(); } }, - _ => None, } } pub(crate) fn imdb_id(&self) -> Option<&str> { match self { MTInfo::MOVIE(movie) => match movie { - MovieType::TMDB() => None, + MovieType::TMDB(movie) => { + return movie.movie.imdb_id(); + } }, MTInfo::TV(tv) => match tv { TVType::TMDB(tv) => { - if let Some(external_ids) = &tv.tv.external_ids { - return Some(external_ids.imdb_id()); - } - return None; + return tv.tv.imdb_id(); } }, } @@ -232,7 +478,7 @@ impl MTInfo { pub(crate) fn tmdb_id_str(&self) -> String { match self { MTInfo::MOVIE(movie) => match movie { - MovieType::TMDB() => "".to_string(), + MovieType::TMDB(movie) => "".to_string(), }, MTInfo::TV(tv) => match tv { TVType::TMDB(tv) => tv.tv.id.to_string(), @@ -243,7 +489,7 @@ impl MTInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum MovieType { - TMDB(), + TMDB(TmdbMovieInfo), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -258,12 +504,131 @@ pub enum MTType { } /// 资源类型 -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ResourceType { /// 影视 MT + /// Movie or TV MT, } +impl std::fmt::Display for ResourceType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + ResourceType::MT => "mt", + }; + s.fmt(f) + } +} +impl std::str::FromStr for ResourceType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "mt" => Ok(Self::MT), + _ => Err(format!("Unknown type: {s}")), + } + } +} + +/// 文件名识别上下文,用于实现一些能力 +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MetaContext { + // 父刮削的路径 + pub parent_path: String, + // 当前刮削的路径 + pub cur_path: String, + // 上一次刮削的路径 + pub last_path: String, + // 当前刮削的文件名 + pub cur_file_name: String, + // 当前刮削的规则 + pub cur_rule: String, + // 是否出错 + pub error: bool, + // 当前刮削的输入 + pub input: String, + // 最后一次使用的解析规则 + pub last_rule: Option, +} + +impl MetaContext { + pub fn new() -> MetaContext { + return MetaContext { + cur_path: "".to_string(), + cur_file_name: "".to_string(), + cur_rule: "".to_string(), + last_path: "".to_string(), + parent_path: "".to_string(), + input: "".to_string(), + error: false, + last_rule: None, + }; + } + + // 同一个父路径可以复用上一次的规则 + pub fn enable_cache(&mut self) -> bool { + // todo + // 同一个父路径可以复用上一次的规则 + if let Some(parent) = Path::new(&self.cur_path).parent() { + let parent_path = parent.to_str().unwrap(); + let enable_cache = if parent_path.is_empty() { + false + } else { + if self.last_path.is_empty() { + self.last_path = self.cur_path.clone(); + false + } else { + let last_parent_path = Path::new(&self.last_path).parent().unwrap().to_str().unwrap(); + if last_parent_path == parent_path { + tracing::debug!( + "build_mt_file_tokens parent_path = {} last_parent_path = {}", + parent_path, + last_parent_path + ); + true + } else { + self.last_path = self.cur_path.clone(); + false + } + } + }; + return enable_cache; + } + return false; + } + + pub(crate) fn reset(&mut self) { + self.cur_path = "".to_string(); + self.cur_file_name = "".to_string(); + self.cur_rule = "".to_string(); + self.last_path = "".to_string(); + self.parent_path = "".to_string(); + self.error = false; + } + + pub fn init(&mut self, src_path: &str) -> bool { + self.reset(); + + let path = Path::new(&src_path); + let parent_path = path.parent().unwrap().to_str().unwrap().to_string(); + + // 检查是否有错误,如果有错误,那么跳过 + if !self.parent_path.is_empty() && parent_path == self.parent_path && self.error { + tracing::info!(target: "soda::info","刮削失败,跳过: {}", src_path); + return false; + } else { + self.error = false; + } + + // 更新刮削上下文 + self.parent_path = parent_path; + self.cur_path = src_path.to_string(); + self.cur_file_name = path.file_name().unwrap().to_str().unwrap().to_string(); + + return true; + } +} + /// The metadata parsed from the soda file name. /// /// eg: 凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4 @@ -282,6 +647,18 @@ pub struct MTMetadata { ///a pub title_cn: String, + /// english title aka + /// + /// Sseo-ni.AKA.Sunny + /// + pub aka_title_en: String, + + /// aka part 1 + pub aka_title_en_first: String, + + /// aka part 2 + pub aka_title_en_second: String, + /// english title /// /// The.Mortal.Ascention @@ -292,7 +669,7 @@ pub struct MTMetadata { /// /// 2020 /// - pub year: Option, + pub year: String, /// 发布年,TMDB补充 pub release_year: Option, @@ -465,7 +842,7 @@ impl MTMetadata { origin_title: title.to_string(), title_cn: "".to_string(), title_en: "".to_string(), - year: None, + year: "".to_string(), season: "".to_string(), episode: "".to_string(), resolution: "".to_string(), @@ -476,6 +853,9 @@ impl MTMetadata { release_group: "".to_string(), special: "".to_string(), release_year: None, + aka_title_en: "".to_string(), + aka_title_en_first: "".to_string(), + aka_title_en_second: "".to_string(), }; } @@ -488,18 +868,50 @@ impl MTMetadata { } pub fn is_empty(&self) -> bool { - return self.title_cn.is_empty() && self.title_en.is_empty() && self.year.is_none() && self.season.is_empty() && self.episode.is_empty() && self.resolution.is_empty() && self.source.is_empty() && self.extension.is_empty() && self.video_codec.is_empty() && self.audio_codec.is_empty() && self.release_group.is_empty(); + return self.title_cn.is_empty() + && self.title_en.is_empty() + && self.year.is_empty() + && self.season.is_empty() + && self.episode.is_empty() + && self.resolution.is_empty() + && self.source.is_empty() + && self.extension.is_empty() + && self.video_codec.is_empty() + && self.audio_codec.is_empty() + && self.release_group.is_empty(); } pub fn title(&self) -> &str { return if self.title_cn.is_empty() { &self.title_en } else { &self.title_cn }; } + pub fn episode_number_format(&self) -> String { + if self.episode.is_empty() { + return "".to_string(); + } + return format!("E{:02}", self.episode_number().unwrap_or(0)); + } + + pub fn season_number_format(&self) -> String { + if self.season.is_empty() { + return "".to_string(); + } + return format!("S{:02}", self.season_number().unwrap_or(0)); + } + pub fn season_number(&self) -> Option { if self.season.is_empty() { return None; } - let season_number = self.season.split("S").collect::>().get(1).unwrap().to_string().parse::().expect("season number parse error"); + let season_number = self + .season + .split("S") + .collect::>() + .get(1) + .unwrap() + .to_string() + .parse::() + .expect("season number parse error"); return Some(season_number); } @@ -507,21 +919,122 @@ impl MTMetadata { if self.episode.is_empty() { return None; } - let episode_number = self.episode.split("E").collect::>().get(1).unwrap().to_string().parse::().expect("episode number parse error"); + let episode_number = self + .episode + .split("E") + .collect::>() + .get(1) + .unwrap() + .to_string() + .parse::() + .expect("episode number parse error"); return Some(episode_number); } - pub(crate) fn merge(&mut self, info: &mut MTInfo) { + pub(crate) fn merge_movie(&mut self, info: &mut MTInfo) { + if let MTInfo::MOVIE(movie) = info { + if let MovieType::TMDB(movie) = movie { + if self.is_movie() { + // 如果没有中文名,则合并中文名 + if self.title_cn.is_empty() && !movie.movie.name().is_empty() { + let new_title = movie.movie.name().to_string(); + + // 新标题不等于英文名才合并 + if new_title != self.title_en { + if contains_invalid_chars(&new_title) { + tracing::debug!("invalid title_cn, title = {}", new_title); + } else { + tracing::debug!("merge title_cn, old_title = {} new_title = {}", self.title_cn, new_title); + self.title_cn = new_title; + } + } + } + + // 如果没有英文名,则合并英文名 + if self.title_en.is_empty() && !movie.movie.original_name().is_empty() { + let new_title = movie.movie.original_name().to_string(); + + // 新标题不等于中文名才合并 + if new_title != self.title_cn { + tracing::debug!("merge title_en, old_title = {} new_title = {}", self.title_en, new_title); + self.title_en = new_title; + } + } + + // 补充发布年 + if self.release_year.is_none() && !movie.movie.first_air_date().is_empty() && movie.movie.first_air_date().len() > 4 { + self.release_year = Some(movie.movie.first_air_date()[0..4].to_string()); + tracing::debug!( + "merge release_year, release_year = {:?} first_air_date = {:?}", + self.release_year, + movie.movie.first_air_date() + ); + } + + // 如果英文名不一致,但是小写是一直的,那么合并英文名 + // 锻刀大赛.Forged.in.Fire.2022.S09 == 锻刀大赛.forged.in.fire.2022.S09 + if !self.title_en.is_empty() + && !movie.movie.original_name().is_empty() + && self.title_en.to_lowercase() == movie.movie.original_name().to_lowercase() + && self.title_en != movie.movie.original_name() + { + tracing::debug!( + "merge title_en, old_title = {} new_title = {}", + self.title_en, + movie.movie.original_name() + ); + self.title_en = movie.movie.original_name().to_string(); + } + } + } + } + } + + pub(crate) fn merge_tv(&mut self, info: &mut MTInfo) { if let MTInfo::TV(tv) = info { if let TVType::TMDB(tv) = tv { if self.is_tv() { - // 如果是TV但是有集无季需要补充信息 + // 如果季相同则更新年份 if let Some(seasons) = &tv.tv.seasons { - if seasons.len() == 1 { - if let Some(season) = seasons.get(0) { - if let Some(season_number) = season.season_number { - self.season = format!("S{:02}", season_number); - tracing::info!("merge season info, name = {} season = {}", self.title(), self.season); + for season in seasons { + if season.season_number() == self.season_number().unwrap_or(0).to_string() + && !self.year.is_empty() + && !season.air_date().is_empty() + && season.air_date().len() > 4 + { + let year: String = self.year.to_string(); + let season_release_year = season.air_date()[0..4].to_string(); + if year != season_release_year { + self.year = season_release_year; + tracing::debug!("merge year, old_year = {} new_year = {}", year, self.year); + } + } + } + } + + // 如果有集无季需要补充季信息 + if self.season.is_empty() && !self.episode.is_empty() { + if let Some(seasons) = &tv.tv.seasons { + for season in seasons { + // 有年有季的信息 + if !self.year.is_empty() && !season.air_date().is_empty() && season.air_date().len() > 4 { + let year = self.year.to_string(); + let season_release_year = season.air_date()[0..4].to_string(); + if year == season_release_year { + if let Some(season_number) = season.season_number { + self.season = format!("S{:02}", season_number); + tracing::debug!("merge season info, name = {} season = {}", self.origin_title, self.season); + break; + } + } + } + // 无年有季的信息 + else if self.year.is_empty() && !season.air_date().is_empty() && season.air_date().len() > 4 { + if let Some(season_number) = season.season_number { + self.season = format!("S{:02}", season_number); + tracing::debug!("merge season info, name = {} season = {}", self.origin_title, self.season); + break; + } } } } @@ -530,26 +1043,65 @@ impl MTMetadata { // 如果没有中文名,则合并中文名 if self.title_cn.is_empty() && !tv.tv.name().is_empty() { let new_title = tv.tv.name().to_string(); - tracing::info!("merge title_cn, old_title = {} new_title = {}", self.title_cn, new_title); - self.title_cn = new_title; + + // 新标题不等于英文名才合并 + if new_title != self.title_en { + if contains_invalid_chars(&new_title) { + tracing::debug!("invalid title_cn, title = {}", new_title); + } else { + tracing::debug!("merge title_cn, old_title = {} new_title = {}", self.title_cn, new_title); + self.title_cn = new_title; + } + } } // 如果没有英文名,则合并英文名 if self.title_en.is_empty() && !tv.tv.original_name().is_empty() { let new_title = tv.tv.original_name().to_string(); - tracing::info!("merge title_en, old_title = {} new_title = {}", self.title_en, new_title); - self.title_en = new_title; + + // 新标题不等于中文名才合并 + if new_title != self.title_cn { + tracing::debug!("merge title_en, old_title = {} new_title = {}", self.title_en, new_title); + self.title_en = new_title; + } } // 补充发布年 if self.release_year.is_none() && !tv.tv.first_air_date().is_empty() && tv.tv.first_air_date().len() > 4 { self.release_year = Some(tv.tv.first_air_date()[0..4].to_string()); - tracing::info!("merge release_year, release_year = {:?} first_air_date = {:?}", self.release_year, tv.tv.first_air_date()); + tracing::debug!( + "merge release_year, release_year = {:?} first_air_date = {:?}", + self.release_year, + tv.tv.first_air_date() + ); + } + + // 如果英文名不一致,但是小写是一直的,那么合并英文名 + // 锻刀大赛.Forged.in.Fire.2022.S09 == 锻刀大赛.forged.in.fire.2022.S09 + if !self.title_en.is_empty() + && !tv.tv.original_name().is_empty() + && self.title_en.to_lowercase() == tv.tv.original_name().to_lowercase() + && self.title_en != tv.tv.original_name() + { + tracing::debug!("merge title_en, old_title = {} new_title = {}", self.title_en, tv.tv.original_name()); + self.title_en = tv.tv.original_name().to_string(); } } } } } + + pub(crate) fn merge_season(&mut self, season_detail: &TmdbSeason) { + // 如果季没有年份则合并一下年份 + if self.year.is_empty() && season_detail.air_date().len() > 4 { + self.year = season_detail.air_date()[0..4].to_string(); + } + } +} + +fn contains_invalid_chars(s: &str) -> bool { + let invalid_chars = ['<', '>', ':', '"', '|', '?', '*']; + s.chars().any(|c| invalid_chars.contains(&c)) } #[derive(Debug, Clone)] @@ -581,5 +1133,168 @@ pub enum TransferType { Move, } +impl std::fmt::Display for TransferType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + TransferType::HardLink => "hard_link", + TransferType::SymbolLink => "symbol_link", + TransferType::Copy => "copy", + TransferType::Move => "move", + }; + s.fmt(f) + } +} + +impl std::str::FromStr for TransferType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "hard_link" => Ok(Self::HardLink), + "symbol_link" => Ok(Self::SymbolLink), + "copy" => Ok(Self::Copy), + "move" => Ok(Self::Move), + _ => Err(format!("Unknown type: {s}")), + } + } +} + +#[derive(Debug, Clone)] +pub struct Token { + pub tokens: HashMap, +} + +impl Token { + pub fn new() -> Token { + return Token { tokens: HashMap::new() }; + } +} + #[cfg(test)] mod entity_tests {} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Rule { + pub rule: String, + pub replaces: Option>, + pub regex_replaces: Option>, +} + +impl Rule { + pub fn update(&mut self, rule: Rule) { + self.rule = rule.rule; + self.replaces = rule.replaces; + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RuleReplaces { + pub src: String, + pub target: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MatchRule { + pub before_replaces: Option>, + pub rules: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RegexRule { + // pub original_title_en: Vec, + pub edition: Vec, + pub version: Vec, + pub resolution_cn: Vec, + pub episode_title_jp: Vec, + pub episode_title_cn: Vec, + pub episode_title_en: Vec, + pub title_number_en: Vec, + pub title_en: Vec, + pub title_number_cn: Vec, + pub season_title_cn: Vec, + pub title_cn: Vec, + pub subtitle_en: Vec, + pub country: Vec, + pub resolution: Vec, + pub source: Vec, + pub company: Vec, + pub video_codec: Vec, + pub color: Vec, + pub audio_codec: Vec, + pub release_group: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) struct NameMap { + pub(crate) src: NameInfo, + pub(crate) target: NameInfo, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) struct NameInfo { + pub(crate) title_cn: String, + pub(crate) title_en: String, + pub(crate) release_year: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) struct NamesMap { + pub(crate) names: Vec, +} + +pub(crate) const KEY_ORIGIN_TITLE: &str = "origin_title"; +pub(crate) const KEY_TITLE_CN: &str = "title_cn"; +pub(crate) const KEY_TITLE_EN: &str = "title_en"; +pub(crate) const KEY_AKA_TITLE_EN: &str = "aka_title_en"; +pub(crate) const KEY_AKA_TITLE_EN_FIRST: &str = "aka_title_en_first"; +pub(crate) const KEY_AKA_TITLE_EN_SECOND: &str = "aka_title_en_second"; +pub(crate) const KEY_VIDEO_CODEC: &str = "video_codec"; +pub(crate) const KEY_AUDIO_CODEC: &str = "audio_codec"; +pub(crate) const KEY_SOURCE: &str = "source"; +pub(crate) const KEY_RESOLUTION: &str = "resolution"; +pub(crate) const KEY_YEAR: &str = "year"; +pub(crate) const KEY_SEASON: &str = "season"; +pub(crate) const KEY_EPISODE: &str = "episode"; +pub(crate) const KEY_EPISODE_1: &str = "episode1"; +pub(crate) const KEY_EPISODE_2: &str = "episode2"; +pub(crate) const KEY_RELEASE_GROUP: &str = "release_group"; +pub(crate) const KEY_SPECIAL: &str = "special"; +pub(crate) const KEY_COMPANY: &str = "company"; +pub(crate) const KEY_COLOR: &str = "color"; +pub(crate) const KEY_EDITION: &str = "edition"; + +pub(crate) const KEY_SEASON_TITLE_CN: &str = "season_title_cn"; +pub(crate) const KEY_TITLE_NUMBER_CN: &str = "title_number_cn"; +pub(crate) const KEY_TITLE_NUMBER_EN: &str = "title_number_en"; +pub(crate) const KEY_EPISODE_TITLE_EN: &str = "episode_title_en"; +pub(crate) const KEY_EPISODE_TITLE_CN: &str = "episode_title_cn"; +pub(crate) const KEY_EPISODE_TITLE_JP: &str = "episode_title_jp"; +pub(crate) const KEY_TITLE_YEAR: &str = "title_year"; +pub(crate) const KEY_TEXT_CN: &str = "text_cn"; +pub(crate) const KEY_YEAR_START_TO_END: &str = "year_start_to_end"; +pub(crate) const KEY_YEAR_MONTH_DAY: &str = "year_month_day"; +pub(crate) const KEY_SEASON_EPISODE: &str = "season_episode"; +pub(crate) const KEY_SEASON_EPISODE_EPISODE: &str = "season_episode_episode"; +pub(crate) const KEY_SEASON_NUMBER: &str = "season_number"; +pub(crate) const KEY_EPISODE_NUMBER: &str = "episode_number"; +pub(crate) const KEY_SEASON_CN: &str = "season_cn"; +pub(crate) const KEY_EPISODE_CN: &str = "episode_cn"; +pub(crate) const KEY_SEASON_ALL_CN: &str = "season_all_cn"; +pub(crate) const KEY_SEASON_START_TO_END_CN: &str = "season_start_to_end_cn"; +pub(crate) const KEY_SEASON_START_TO_END_EN: &str = "season_start_to_end_en"; +pub(crate) const KEY_RESOLUTION_CN: &str = "resolution_cn"; +pub(crate) const KEY_VERSION: &str = "version"; +pub(crate) const KEY_SUBTITLE_EN: &str = "subtitle_en"; +pub(crate) const KEY_SUBTITLE_CN: &str = "subtitle_cn"; +pub(crate) const KEY_SUBTITLE: &str = "subtitle"; +pub(crate) const KEY_AUDIO_CN: &str = "audio_cn"; +pub(crate) const KEY_ANYTHING: &str = "anything"; +pub(crate) const KEY_COUNTRY: &str = "country"; +pub(crate) const KEY_MIX_NUMBERS_LETTERS: &str = "mix_numbers_letters"; +pub(crate) const KEY_NUMBER: &str = "number"; +pub(crate) const KEY_EXTENSION: &str = "extension"; +pub(crate) const KEY_AKA: &str = "AKA"; + +pub(crate) fn wrap(s: &str) -> String { + return format!("${}$", s); +} diff --git a/soda_resource_tools_lib/src/soda/extension_option.rs b/soda_resource_tools_lib/src/soda/extension_option.rs index f11d39bf..94924d5e 100644 --- a/soda_resource_tools_lib/src/soda/extension_option.rs +++ b/soda_resource_tools_lib/src/soda/extension_option.rs @@ -73,7 +73,7 @@ impl OptionExtensions for Option { } /// - /// let api_key = get_api_key().on_none_inspect(|| { tracing::info!("api key is none"); })?; + /// let api_key = get_api_key().on_none_inspect(|| { tracing::debug!("api key is none"); })?; /// fn on_none_inspect(self, f: F) -> Self where diff --git a/soda_resource_tools_lib/src/soda/fanart.rs b/soda_resource_tools_lib/src/soda/fanart.rs index 42545a48..ddf09277 100644 --- a/soda_resource_tools_lib/src/soda/fanart.rs +++ b/soda_resource_tools_lib/src/soda/fanart.rs @@ -1,6 +1,6 @@ use serde_json::Value; -use self::entity::FanartTV; +use self::entity::{FanartMovie, FanartTV}; use super::{ entity::{MTInfo, SodaError}, @@ -182,13 +182,27 @@ pub(crate) mod entity; /// ``` pub(crate) fn obtain_images(mt_info: &mut MTInfo) { match mt_info { - MTInfo::MOVIE(_) => todo!(), + MTInfo::MOVIE(movie) => match movie { + super::entity::MovieType::TMDB(info) => { + let tmdb_id = info.movie.tmdb_id(); + if !tmdb_id.is_empty() { + match request_fanart_movies(&tmdb_id) { + Ok(fanart) => { + info.fanart = Some(fanart); + } + Err(e) => { + tracing::error!("request_fanart_movie error {:?}", e); + } + } + } + } + }, MTInfo::TV(tv) => match tv { super::entity::TVType::TMDB(info) => { if let Some(tvdb_id) = info.tv.tvdb_id() { match request_fanart_tv(&tvdb_id) { - Ok(fanart_tv) => { - info.fanart_tv = Some(fanart_tv); + Ok(fanart) => { + info.fanart = Some(fanart); } Err(e) => { tracing::error!("request_fanart_tv error {:?}", e); @@ -204,28 +218,373 @@ fn get_api_key() -> String { "fe74657e137cca7ca521c12e44fd6292".to_string() } -// https://fanart.tv/ -// https://fanarttv.docs.apiary.io/#introduction/api_key-and-client_key +/// https://fanart.tv/ +/// https://fanarttv.docs.apiary.io/#introduction/api_key-and-client_key ///_movie_url: str = f'https://webservice.fanart.tv/v3/movies/%s?api_key={settings.FANART_API_KEY}' -// _tv_url: str = f'https://webservice.fanart.tv/v3/tv/%s?api_key={settings.FANART_API_KEY}' +/// _tv_url: str = f'https://webservice.fanart.tv/v3/tv/%s?api_key={settings.FANART_API_KEY}' +/// https://webservice.fanart.tv/v3/tv/384640?api_key=7b7fb929ae578432f178b94e75a6e663 fn request_fanart_tv(tvdb_id: &str) -> Result { let url = format!("https://webservice.fanart.tv/v3/tv/{}?api_key={}", tvdb_id, get_api_key()); let json = blocking_request_value_with_cache(super::cache::CacheType::TMDB, "GET", &url)?; match serde_json::from_value(json) { Ok(value) => Ok(value), - Err(e) => { - if e.is_data() { - tracing::error!("数据类型错误 {}", e); - } else if e.is_syntax() { - tracing::error!("语法错误 {}", e); - } else if e.is_io() { - tracing::error!("IO 错误 {}", e); - } else if e.is_eof() { - tracing::error!("意外的文件结束 {}", e); - } - Err(SodaError::Json(e)) - } + Err(e) => Err(SodaError::Json(e)), + } +} + +/// https://fanarttv.docs.apiary.io/#reference/movies/get-movies/get-images-for-movie +/// https://webservice.fanart.tv/v3/movies/372058?api_key=7b7fb929ae578432f178b94e75a6e663 +/// ```json +/// { +/// "name": "Your Name.", +/// "tmdb_id": "372058", +/// "imdb_id": "tt5311514", +/// "hdmovielogo": [ +/// { +/// "id": "179358", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovielogo/your-name-587d8cb5bef81.png", +/// "lang": "en", +/// "likes": "7" +/// }, +/// { +/// "id": "130708", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovielogo/your-name-566af0546b9d8.png", +/// "lang": "ja", +/// "likes": "2" +/// }, +/// { +/// "id": "173213", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovielogo/your-name-5845e9e8eed81.png", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "173195", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovielogo/your-name-5845db34263d4.png", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "212924", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovielogo/your-name-5a391e5c3871f.png", +/// "lang": "ru", +/// "likes": "1" +/// }, +/// { +/// "id": "276373", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovielogo/your-name-5dbd5727a99c8.png", +/// "lang": "ja", +/// "likes": "0" +/// }, +/// { +/// "id": "373543", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovielogo/your-name-62e91ca365271.png", +/// "lang": "zh", +/// "likes": "0" +/// } +/// ], +/// "moviebackground": [ +/// { +/// "id": "173208", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-5845e4f97e1b1.jpg", +/// "lang": "", +/// "likes": "4" +/// }, +/// { +/// "id": "191798", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-58f8e1b587811.jpg", +/// "lang": "", +/// "likes": "4" +/// }, +/// { +/// "id": "130714", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-566af6943c991.jpg", +/// "lang": "", +/// "likes": "3" +/// }, +/// { +/// "id": "173241", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-58461266f328d.jpg", +/// "lang": "", +/// "likes": "2" +/// }, +/// { +/// "id": "173204", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-5845e453ddf5f.jpg", +/// "lang": "", +/// "likes": "2" +/// }, +/// { +/// "id": "173201", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-5845e0146e532.jpg", +/// "lang": "", +/// "likes": "2" +/// }, +/// { +/// "id": "130713", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-566af4cd73697.jpg", +/// "lang": "", +/// "likes": "1" +/// }, +/// { +/// "id": "173206", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-5845e48055152.jpg", +/// "lang": "", +/// "likes": "1" +/// }, +/// { +/// "id": "173207", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebackground/your-name-5845e4a0c6d41.jpg", +/// "lang": "", +/// "likes": "1" +/// } +/// ], +/// "movieposter": [ +/// { +/// "id": "173198", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5845df3ac3d16.jpg", +/// "lang": "00", +/// "likes": "4" +/// }, +/// { +/// "id": "130709", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-566af06fcb6f5.jpg", +/// "lang": "ja", +/// "likes": "3" +/// }, +/// { +/// "id": "173200", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5845dfaf75329.jpg", +/// "lang": "en", +/// "likes": "3" +/// }, +/// { +/// "id": "216544", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a6c6dd7aa9b0.jpg", +/// "lang": "en", +/// "likes": "3" +/// }, +/// { +/// "id": "216542", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a6c6dbf2e633.jpg", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "212110", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a2db632e8087.jpg", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "216543", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a6c6dcbcc520.jpg", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "216545", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a6c6de35a75a.jpg", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "397624", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-648f55f3ed34e.jpg", +/// "lang": "00", +/// "likes": "2" +/// }, +/// { +/// "id": "173199", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5845df4683e35.jpg", +/// "lang": "ja", +/// "likes": "2" +/// }, +/// { +/// "id": "212925", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a391e78462c3.jpg", +/// "lang": "ru", +/// "likes": "1" +/// }, +/// { +/// "id": "130710", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-566af0a3b7870.jpg", +/// "lang": "ja", +/// "likes": "1" +/// }, +/// { +/// "id": "212116", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a2dc19f656cf.jpg", +/// "lang": "en", +/// "likes": "1" +/// }, +/// { +/// "id": "212109", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a2db6271f5b9.jpg", +/// "lang": "en", +/// "likes": "1" +/// }, +/// { +/// "id": "212108", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5a2db61a609f5.jpg", +/// "lang": "en", +/// "likes": "1" +/// }, +/// { +/// "id": "173212", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5845e8716ddd9.jpg", +/// "lang": "00", +/// "likes": "1" +/// }, +/// { +/// "id": "303081", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/movieposter/your-name-5efcd1142897c.jpg", +/// "lang": "ja", +/// "likes": "0" +/// } +/// ], +/// "moviedisc": [ +/// { +/// "id": "210331", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviedisc/your-name-5a11496732508.png", +/// "lang": "es", +/// "likes": "3", +/// "disc": "1", +/// "disc_type": "bluray" +/// }, +/// { +/// "id": "210332", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviedisc/your-name-5a1149794b5e2.png", +/// "lang": "en", +/// "likes": "2", +/// "disc": "1", +/// "disc_type": "bluray" +/// }, +/// { +/// "id": "130779", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviedisc/your-name-566b968bcf999.png", +/// "lang": "ja", +/// "likes": "2", +/// "disc": "1", +/// "disc_type": "bluray" +/// }, +/// { +/// "id": "130778", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviedisc/your-name-566b966fd82af.png", +/// "lang": "ja", +/// "likes": "1", +/// "disc": "1", +/// "disc_type": "bluray" +/// }, +/// { +/// "id": "212114", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviedisc/your-name-5a2db6715f8f0.png", +/// "lang": "en", +/// "likes": "1", +/// "disc": "1", +/// "disc_type": "bluray" +/// }, +/// { +/// "id": "212113", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviedisc/your-name-5a2db6659fd6a.png", +/// "lang": "en", +/// "likes": "1", +/// "disc": "1", +/// "disc_type": "bluray" +/// }, +/// { +/// "id": "212112", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviedisc/your-name-5a2db65a262f2.png", +/// "lang": "en", +/// "likes": "1", +/// "disc": "1", +/// "disc_type": "bluray" +/// }, +/// { +/// "id": "212111", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviedisc/your-name-5a2db643ddc80.png", +/// "lang": "en", +/// "likes": "1", +/// "disc": "1", +/// "disc_type": "bluray" +/// } +/// ], +/// "moviethumb": [ +/// { +/// "id": "210334", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviethumb/your-name-5a114afcd27c4.jpg", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "173796", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviethumb/your-name-584c67e0de0f1.jpg", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "212929", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviethumb/your-name-5a391ecd68ca8.jpg", +/// "lang": "ru", +/// "likes": "1" +/// }, +/// { +/// "id": "368161", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviethumb/your-name-628592f53d488.jpg", +/// "lang": "en", +/// "likes": "0" +/// } +/// ], +/// "moviebanner": [ +/// { +/// "id": "173211", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebanner/your-name-5845e7135b5c2.jpg", +/// "lang": "en", +/// "likes": "2" +/// }, +/// { +/// "id": "212928", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebanner/your-name-5a391eb4b3f91.jpg", +/// "lang": "ru", +/// "likes": "1" +/// }, +/// { +/// "id": "346384", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/moviebanner/your-name-6149cf35f09e0.jpg", +/// "lang": "ja", +/// "likes": "0" +/// } +/// ], +/// "hdmovieclearart": [ +/// { +/// "id": "212926", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovieclearart/your-name-5a391e8f8c516.png", +/// "lang": "en", +/// "likes": "1" +/// }, +/// { +/// "id": "212927", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovieclearart/your-name-5a391ea639497.png", +/// "lang": "ru", +/// "likes": "1" +/// }, +/// { +/// "id": "130711", +/// "url": "https://assets.fanart.tv/fanart/movies/372058/hdmovieclearart/your-name-566af0dd41b1d.png", +/// "lang": "ja", +/// "likes": "1" +/// } +/// ] +/// } +/// ``` +fn request_fanart_movies(tvdb_id: &str) -> Result { + let url = format!("https://webservice.fanart.tv/v3/movies/{}?api_key={}", tvdb_id, get_api_key()); + let json = blocking_request_value_with_cache(super::cache::CacheType::TMDB, "GET", &url)?; + + match serde_json::from_value(json) { + Ok(value) => Ok(value), + Err(e) => Err(SodaError::Json(e)), } } @@ -234,8 +593,8 @@ mod fanart_tests { use super::request_fanart_tv; // #[test] - fn test_request_fanart_tv() { - let ret = request_fanart_tv(&384640.to_string()).unwrap(); - println!("{:?}", ret); - } + // fn test_request_fanart_tv() { + // let ret = request_fanart_tv(&384640.to_string()).unwrap(); + // println!("{:?}", ret); + // } } diff --git a/soda_resource_tools_lib/src/soda/fanart/entity.rs b/soda_resource_tools_lib/src/soda/fanart/entity.rs index 176f78d4..99362829 100644 --- a/soda_resource_tools_lib/src/soda/fanart/entity.rs +++ b/soda_resource_tools_lib/src/soda/fanart/entity.rs @@ -14,6 +14,20 @@ pub struct FanartTV { pub tvposter: Option>, } +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FanartMovie { + pub name: Option, + pub tmdb_id: Option, + pub imdb_id: Option, + pub hdmovielogo: Option>, + pub moviebackground: Option>, + pub movieposter: Option>, + pub moviedisc: Option>, + pub moviethumb: Option>, + pub moviebanner: Option>, + pub hdmovieclearart: Option>, +} + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FanartImg { pub id: Option, @@ -22,6 +36,7 @@ pub struct FanartImg { pub likes: Option, pub season: Option, } + impl FanartImg { pub(crate) fn url(&self) -> &str { match &self.url { diff --git a/soda_resource_tools_lib/src/soda/filebrowser.rs b/soda_resource_tools_lib/src/soda/filebrowser.rs index 4d8de97c..081d4943 100644 --- a/soda_resource_tools_lib/src/soda/filebrowser.rs +++ b/soda_resource_tools_lib/src/soda/filebrowser.rs @@ -33,11 +33,22 @@ impl FileItem { pub(crate) fn create(path_obj: &Path) -> FileItem { let is_dir = path_obj.is_dir(); let file_type = if is_dir { "dir".to_string() } else { "file".to_string() }; - let path = if system::is_windows_os() { path_obj.to_str().unwrap().to_string().replace("\\", "/") } else { path_obj.to_str().unwrap().to_string() }; + let path = if system::is_windows_os() { + path_obj.to_str().unwrap().to_string().replace("\\", "/") + } else { + path_obj.to_str().unwrap().to_string() + }; let name = path_obj.file_name().unwrap().to_str().unwrap().to_string(); let extension = Self::get_extension(path_obj, is_dir); let size = if is_dir { 0 } else { path_obj.metadata().unwrap().len() }; - let modify_time = path_obj.metadata().unwrap().modified().unwrap().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let modify_time = path_obj + .metadata() + .unwrap() + .modified() + .unwrap() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); FileItem { file_type, path, @@ -67,7 +78,11 @@ impl FileItem { } fn get_extension(path_obj: &Path, is_dir: bool) -> String { - let extension = if is_dir { "".to_string() } else { path_obj.extension().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_string() }; + let extension = if is_dir { + "".to_string() + } else { + path_obj.extension().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_string() + }; extension } @@ -78,7 +93,14 @@ impl FileItem { let name = path_obj.to_str().unwrap().to_string(); let extension = Self::get_extension(path_obj, is_dir); let size = if is_dir { 0 } else { path_obj.metadata().unwrap().len() }; - let modify_time = path_obj.metadata().unwrap().modified().unwrap().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let modify_time = path_obj + .metadata() + .unwrap() + .modified() + .unwrap() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); FileItem { file_type, path, @@ -98,7 +120,14 @@ impl FileItem { let name = path_obj.to_str().unwrap().to_string(); let extension = Self::get_extension(path_obj, is_dir); let size = if is_dir { 0 } else { path_obj.metadata().unwrap().len() }; - let modify_time = path_obj.metadata().unwrap().modified().unwrap().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let modify_time = path_obj + .metadata() + .unwrap() + .modified() + .unwrap() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); FileItem { file_type, path, diff --git a/soda_resource_tools_lib/src/soda/finder.rs b/soda_resource_tools_lib/src/soda/finder.rs index 87c758b6..93571641 100644 --- a/soda_resource_tools_lib/src/soda/finder.rs +++ b/soda_resource_tools_lib/src/soda/finder.rs @@ -8,9 +8,9 @@ use crate::soda::global::REGEX_MT_EXT; use super::entity::ResourceType; use super::utils; -pub(crate) fn get_level1_sub_dirs(directory_path: &str, callback: F) -> std::io::Result<()> +pub(crate) fn get_level1_sub_dirs(directory_path: &str, callback: &mut F) -> std::io::Result<()> where - F: Fn(&Path), + F: FnMut(&Path), { let entries = fs::read_dir(directory_path)?; for entry in entries { @@ -23,9 +23,9 @@ where Ok(()) } -pub(crate) fn get_level1_sub_files_filter_format(directory_path: &str, suffix: &Regex, callback: F) -> std::io::Result<()> +pub(crate) fn get_level1_sub_files_filter_format(directory_path: &str, suffix: &Regex, callback: &mut F) -> std::io::Result<()> where - F: Fn(&Path), + F: FnMut(&Path), { for entry in fs::read_dir(directory_path)? { let entry = entry?; @@ -44,9 +44,9 @@ where Ok(()) } -fn find_mt_with_ext(dir_path: &str, callback: &F) +fn find_mt_with_ext(dir_path: &str, callback: &mut F) where - F: Fn(String), + F: FnMut(String), { let path = Path::new(dir_path); if path.is_file() { @@ -60,39 +60,34 @@ where } } - if let Err(e) = get_level1_sub_files_filter_format(dir_path, ®EX_MT_EXT, |path: &Path| { + let mut callback_file = |path: &Path| { callback(path.to_string_lossy().to_string()); - }) { + }; + if let Err(e) = get_level1_sub_files_filter_format(dir_path, ®EX_MT_EXT, &mut callback_file) { tracing::error!("Error reading files: {}", e); } - if let Err(e) = get_level1_sub_dirs(dir_path, |path: &Path| { + let mut callback_dir = |path: &Path| { // 如果是蓝光目录,不再递归查找 if utils::is_bluray_dir(dir_path) { callback(dir_path.to_string()); } else { find_mt_with_ext(&path.to_string_lossy().to_string(), callback); } - }) { + }; + if let Err(e) = get_level1_sub_dirs(dir_path, &mut callback_dir) { tracing::error!("Error reading directory: {}", e); } } /// 寻找配置目录下的可整理资源 -pub fn find(resource_type: &ResourceType, src_dir_path: &str, callback: F) +pub fn find(resource_type: &ResourceType, src_dir_path: &str, callback: &mut F) where - F: Fn(String), + F: FnMut(String), { - tracing::info!("src_dir_path = {:?}", src_dir_path); - - // 找到要整理的资源 - let find_resource_callback = |path: String| { - callback(path); - }; - match resource_type { ResourceType::MT => { - find_mt_with_ext(src_dir_path, &find_resource_callback); + find_mt_with_ext(src_dir_path, callback); } } } diff --git a/soda_resource_tools_lib/src/soda/global.rs b/soda_resource_tools_lib/src/soda/global.rs index 61493cdb..157b725b 100644 --- a/soda_resource_tools_lib/src/soda/global.rs +++ b/soda_resource_tools_lib/src/soda/global.rs @@ -1,7 +1,7 @@ use once_cell::sync::Lazy; use regex::Regex; -pub(crate) static REGEX_MT_EXT: Lazy = Lazy::new(|| Regex::new(r"\.(mp4|mkv|ts|iso|rmvb|avi|mov|mepg|mpg|wmv|3gp|asf|m4v|flv|m2ts)$").unwrap()); +pub(crate) static REGEX_MT_EXT: Lazy = Lazy::new(|| Regex::new(r"\.(mp4|mkv|rmvb|avi|mov|mepg|mpg|wmv|3gp|asf|m4v|flv)$").unwrap()); #[cfg(test)] mod global_tests { diff --git a/soda_resource_tools_lib/src/soda/meta.rs b/soda_resource_tools_lib/src/soda/meta.rs index 361c34f0..6448a86c 100644 --- a/soda_resource_tools_lib/src/soda/meta.rs +++ b/soda_resource_tools_lib/src/soda/meta.rs @@ -1,85 +1,105 @@ -use self::token::Tokens; use crate::soda::entity::MTMetadata; use crate::soda::global::REGEX_MT_EXT; use crate::soda::utils; use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; use std::path::Path; use std::time::SystemTime; +use super::entity::*; use super::LIB_CONFIG; pub(crate) mod strong_match_token; mod token; /// 识别影音媒体信息 -pub(crate) fn mt_metadata(path: &str) -> Option { - tracing::info!("path = {:?}", path); - let title = Path::new(path).file_name()?.to_str()?.to_string(); +pub fn create_metadata_mt(meta_context: &mut MetaContext) -> Result { + let cur_path = meta_context.cur_path.to_string(); + let cur_file_name = meta_context.cur_file_name.to_string(); + tracing::debug!("create metadata path = {:?}", cur_path); let start_time = SystemTime::now(); - if let Some(tokens) = token::build_tokens(&title) { - if let Some(meta) = gen_mt_metadata(&title, tokens) { + if let Some(tokens) = token::tokens(meta_context) { + if let Some(meta) = gen_metadata_mt(&cur_file_name, tokens) { let end_time = SystemTime::now(); - tracing::info!("title = {:?} time = {:?}", title, (end_time.duration_since(start_time)).unwrap()); - return Some(meta); + tracing::debug!("meta = {:?} ", meta); + tracing::debug!( + "create metadata success title = {:?} time = {:?}", + cur_file_name, + (end_time.duration_since(start_time)).unwrap() + ); + return Ok(meta); } } - tracing::error!("mt_metadata failed, path {:?}", path); - utils::write_meta_file_path(&path); - None + + tracing::error!(target:"soda::info", "识别媒体信息失败: {}", cur_path); + tracing::error!(target:"soda::metadata", "识别媒体信息失败: {}", cur_path); + return Err(SodaError::String(format!("create metadata failed, path = {:?}", cur_path))); } /// 生成影音媒体元数据 -fn gen_mt_metadata(title: &str, tokens: Tokens) -> Option { - let tokens = tokens.tokens; +fn gen_metadata_mt(title: &str, token: Token) -> Option { + let tokens = token.tokens; if !tokens.is_empty() { - let mut metadata = MTMetadata::empty(title); + let mut meta = MTMetadata::empty(title); - if let Some(title_cn) = tokens.get("title_cn") { - metadata.title_cn = title_cn.clone(); + if let Some(value) = tokens.get(KEY_TITLE_CN) { + meta.title_cn = value.clone(); } - if let Some(title_en) = tokens.get("title_en") { - metadata.title_en = title_en.clone(); + if let Some(value) = tokens.get(KEY_TITLE_EN) { + meta.title_en = value.clone(); } - if let Some(format) = tokens.get("format") { - metadata.extension = format.clone(); + if let Some(value) = tokens.get(KEY_AKA_TITLE_EN) { + meta.aka_title_en = value.clone(); } - if let Some(video_codec) = tokens.get("video_codec") { - metadata.video_codec = video_codec.clone(); + if let Some(value) = tokens.get(KEY_AKA_TITLE_EN_FIRST) { + meta.aka_title_en_first = value.clone(); } - if let Some(source) = tokens.get("source") { - metadata.source = source.clone(); + if let Some(value) = tokens.get(KEY_AKA_TITLE_EN_SECOND) { + meta.aka_title_en_second = value.clone(); } - if let Some(resolution) = tokens.get("resolution") { - metadata.resolution = resolution.clone(); + if let Some(value) = tokens.get(KEY_EXTENSION) { + meta.extension = value.clone(); } - if let Some(year) = tokens.get("year") { - metadata.year = Some(year.clone()); + if let Some(value) = tokens.get(KEY_VIDEO_CODEC) { + meta.video_codec = value.clone(); } - if let Some(season) = tokens.get("season") { - metadata.season = season.clone(); + if let Some(value) = tokens.get(KEY_SOURCE) { + meta.source = value.clone(); } - if let Some(episode) = tokens.get("episode") { - metadata.episode = episode.clone(); + if let Some(value) = tokens.get(KEY_RESOLUTION) { + meta.resolution = value.clone(); } - if let Some(audio_codec) = tokens.get("audio_codec") { - metadata.audio_codec = audio_codec.clone(); + if let Some(value) = tokens.get(KEY_YEAR) { + meta.year = value.clone(); } - if let Some(release_group) = tokens.get("release_group") { - metadata.release_group = release_group.clone(); + if let Some(value) = tokens.get(KEY_SEASON) { + meta.season = value.clone(); } - if let Some(special) = tokens.get("special") { - metadata.special = special.clone(); + if let Some(value) = tokens.get(KEY_EPISODE) { + meta.episode = value.clone(); + } + + if let Some(value) = tokens.get(KEY_AUDIO_CODEC) { + meta.audio_codec = value.clone(); + } + + if let Some(value) = tokens.get(KEY_RELEASE_GROUP) { + meta.release_group = value.clone(); + } + + if let Some(special) = tokens.get(KEY_SPECIAL) { + meta.special = special.clone(); let config = LIB_CONFIG.lock().unwrap(); if config.metadata_skip_special { @@ -87,8 +107,7 @@ fn gen_mt_metadata(title: &str, tokens: Tokens) -> Option { return None; } } - - return Some(metadata); + return Some(meta); } None } diff --git a/soda_resource_tools_lib/src/soda/meta/strong_match_token.rs b/soda_resource_tools_lib/src/soda/meta/strong_match_token.rs index 571f1a0b..681e9c08 100644 --- a/soda_resource_tools_lib/src/soda/meta/strong_match_token.rs +++ b/soda_resource_tools_lib/src/soda/meta/strong_match_token.rs @@ -1,112 +1,152 @@ use std::collections::HashMap; use std::error::Error; +use std::f32::consts::E; use std::fs::{self, File}; use std::io::Read; +use std::path::Path; use std::sync::Mutex; use once_cell::sync::Lazy; use regex::Regex; use serde::Deserialize; +use crate::soda::entity::{MatchRule, MetaContext, RegexRule, Rule, Token}; use crate::soda::extension_option::OptionExtensions; use crate::soda::LIB_CONFIG; -use super::token::Tokens; +use crate::soda::entity::*; -#[derive(Deserialize, Debug, Clone)] -struct Rule { - rule: String, - replaces: Option>, -} - -impl Rule { - fn update(&mut self, rule: Rule) { - self.rule = rule.rule; - self.replaces = rule.replaces; - } -} - -#[derive(Deserialize, Debug, Clone)] -struct RuleReplaces { - src: String, - target: String, +pub(crate) fn init() { + let regex_rule = ®EX_RULES; + let tv_rules = &TV_RULES; + let movie_rules = &MOVIE_RULES; } -#[derive(Deserialize, Debug, Clone)] -struct MatchRule { - before_replaces: Option>, - rules: Vec, -} +static LAST_PATH: Lazy> = Lazy::new(|| Mutex::new("".to_string())); -#[derive(Deserialize, Debug, Clone)] -struct RegexRule { - source: Vec, - company: Vec, - video_codec: Vec, - color: Vec, - audio_codec: Vec, - release_group: Vec, -} +static REGEX_RULES: Lazy = Lazy::new(|| { + let config = LIB_CONFIG.lock().unwrap(); -pub(crate) fn init() { - let source = ®EX_RULES; - let before_replaces = &RULES; - let last_rule = &LAST_RULE; -} + let content = if !config.strong_match_regex_rules.is_empty() { + config.strong_match_regex_rules.clone() + } else if !config.strong_match_regex_rules_path.is_empty() { + fs::read_to_string(config.strong_match_regex_rules_path.as_str()).unwrap() + } else { + unreachable!("strong_match_regex_rules and strong_match_regex_rules_path is empty") + }; -static LAST_RULE: Lazy> = Lazy::new(|| Mutex::new(RULES.rules[0].clone())); + serde_json::from_str(&content).unwrap() +}); -static REGEX_RULES: Lazy = Lazy::new(|| { +static TV_RULES: Lazy = Lazy::new(|| { let config = LIB_CONFIG.lock().unwrap(); - let content = fs::read_to_string(config.strong_match_regex_rules_path.as_str()).unwrap(); + let content = if !config.strong_match_rules_tv.is_empty() { + config.strong_match_rules_tv.clone() + } else if !config.strong_match_rules_tv_path.is_empty() { + fs::read_to_string(config.strong_match_rules_tv_path.as_str()).unwrap() + } else { + unreachable!("strong_match_rules_tv and strong_match_rules_tv_path is empty") + }; serde_json::from_str(&content).unwrap() }); -static RULES: Lazy = Lazy::new(|| { +static MOVIE_RULES: Lazy = Lazy::new(|| { let config = LIB_CONFIG.lock().unwrap(); - let content = fs::read_to_string(config.strong_match_rules_path.as_str()).unwrap(); + let content = if !config.strong_match_rules_movie.is_empty() { + config.strong_match_rules_movie.clone() + } else if !config.strong_match_rules_movie_path.is_empty() { + fs::read_to_string(config.strong_match_rules_movie_path.as_str()).unwrap() + } else { + unreachable!("strong_match_rules_movie and strong_match_rules_movie_path is empty") + }; serde_json::from_str(&content).unwrap() }); -pub(crate) fn build_mt_file_tokens(title: &str) -> Option { - let enable_cache = LIB_CONFIG.lock().unwrap().strong_match_regex_enable_cache; +pub(crate) fn build_mt_file_tokens(meta_context: &mut MetaContext, before_token: &mut Token) -> Option { + // TV + if let Some(value) = build_tv_tokens(meta_context, before_token) { + return Some(value); + } - if enable_cache { - let mut last_rule = LAST_RULE.lock().unwrap(); - if let Some(token) = build_tokens(&last_rule, title) { + // MOVIE + if let Some(value) = build_movie_tokens(meta_context, before_token) { + return Some(value); + } + + return None; +} + +fn build_movie_tokens(meta_context: &mut MetaContext, before_token: &mut Token) -> Option { + let input = meta_context.input.to_string(); + tracing::debug!("build_movie_tokens input = {}", input); + meta_context.last_rule = None; + let rules = &MOVIE_RULES.rules; + for rule in rules { + if let Some(mut token) = build_tokens(rule, &input) { + // merge tokens to token + for ele in &before_token.tokens { + token.tokens.insert(ele.0.to_string(), ele.1.to_string()); + } return Some(token); } } + return None; +} - let rules = &RULES.rules; +fn build_tv_tokens(meta_context: &mut MetaContext, before_token: &mut Token) -> Option { + let input = meta_context.input.to_string(); + let enable_cache = meta_context.enable_cache(); + tracing::debug!("build_tv_tokens enable_cache = {}", enable_cache); + meta_context.last_rule = None; + if enable_cache { + if let Some(last_rule) = &meta_context.last_rule { + if let Some(mut token) = build_tokens(&last_rule, &input) { + // merge tokens to token + for ele in &before_token.tokens { + token.tokens.insert(ele.0.to_string(), ele.1.to_string()); + } + return Some(token); + } + } + } + let rules = &TV_RULES.rules; for rule in rules { - if let Some(token) = build_tokens(rule, title) { + if let Some(mut token) = build_tokens(rule, &input) { if enable_cache { - let mut last_rule = LAST_RULE.lock().unwrap(); - last_rule.update(rule.clone()); + meta_context.last_rule = Some(rule.clone()); + } + // merge tokens to token + for ele in &before_token.tokens { + token.tokens.insert(ele.0.to_string(), ele.1.to_string()); } return Some(token); } } return None; + + // 先使用TV规则处理 + + None } -pub(crate) fn build_mt_dir_tokens(title: &str) -> Option { - // let mut last_rule = LAST_RULE.lock().unwrap(); - // if let Some(token) = build_tokens(&last_rule, title) { - // return Some(token); - // } +pub(crate) fn build_mt_dir_tokens(title: &str, before_token: &mut Token) -> Option { + let mut new_title = title.to_string(); - for rule in &RULES.rules { + tracing::debug!("build_mt_file_tokens new_title = {}", new_title); + + for rule in &TV_RULES.rules { let mut new_rule = rule.clone(); new_rule.rule = new_rule.rule.replace(".$extension$", ""); new_rule.rule = new_rule.rule.replace("$season_episode$", "$season$"); - if let Some(token) = build_tokens(&new_rule, title) { - // last_rule.update(rule.clone()); + if let Some(mut token) = build_tokens(&new_rule, &new_title) { + // merge tokens to token + for ele in &before_token.tokens { + token.tokens.insert(ele.0.to_string(), ele.1.to_string()); + } return Some(token); } } @@ -116,10 +156,15 @@ pub(crate) fn build_mt_dir_tokens(title: &str) -> Option { /// 匹配中文 /// (?P...):这是一个命名捕获组,名为 "title_cn"。捕获组可以用来从匹配的文本中提取出特定部分。 /// [:\p{Script=Han}\p{Script=Hiragana}]:这是一个字符集,匹配任何在集合中的字符。这个集合包括冒号(:),以及任何属于汉字(Han)或平假名(Hiragana)的字符。 +/// [A-Z]?: 后面可能跟着一个大写字母([A-Z]? 表示匹配0次或1次大写字母) /// +:这表示前面的字符集可以在字符串中出现一次或多次。 /// 所以,整个表达式的意思是:匹配一次或多次的冒号,汉字或平假名字符,并将匹配的部分作为名为 "title_cn" 的捕获组。 -fn regex_item_title_cn() -> &'static str { - r"(?P[:\p{Script=Han}\p{Script=Hiragana}]+)" +fn regex_item_title_cn() -> String { + return regex_item_multi(KEY_TITLE_CN, 0, ®EX_RULES.title_cn); +} + +fn regex_item_season_title_cn() -> String { + return regex_item_multi(KEY_SEASON_TITLE_CN, 0, ®EX_RULES.season_title_cn); } /// 匹配中文 @@ -130,154 +175,210 @@ fn regex_item_title_cn() -> &'static str { /// [:\p{Script=Han}\p{Script=Hiragana}]:这是另一个字符集,匹配任何在集合中的字符。这个集合包括冒号(:),以及任何属于汉字(Han)或平假名(Hiragana)的字符。 /// +:这表示前面的字符集可以在字符串中出现一次或多次。 /// 所以,整个表达式的意思是:匹配一次或多次的数字、字母,后面跟着一次或多次的冒号,汉字或平假名字符,并将匹配的部分作为名为 "name" 的捕获组。 -fn regex_item_title_number_cn() -> &'static str { - r"(?P[0-9A-Za-z]+[:\p{Script=Han}\p{Script=Hiragana}]+)" +fn regex_item_title_number_cn() -> String { + return regex_item_multi(KEY_TITLE_NUMBER_CN, 0, ®EX_RULES.title_number_cn); } /// 匹配英文名称 -fn regex_item_title_en() -> &'static str { - r"(?P[I&'-. A-Za-z]+)" +fn regex_item_title_en() -> String { + return regex_item_multi(KEY_TITLE_EN, 0, ®EX_RULES.title_en); } +/// 匹配AKA原始名称 +/// "original_title_en": [ +/// "AKA\\.[-A-Za-z]+", +/// "[-A-Za-z]+\\.AKA" +/// ], +// fn regex_item_original_title_en() -> String { +// return regex_item_multi("original_title_en", 0, ®EX_RULES.original_title_en); +// } + /// 匹配英文名称 /// 这是一个正则表达式,用于匹配一种特定的字符串模式。下面是各部分的解释: /// (?P...):这是一个命名捕获组,名为 "title_number_en"。捕获组可以用来从匹配的文本中提取出特定部分。 /// [\\()&'-. 0-9A-Za-z]:这是一个字符集,匹配任何在集合中的字符。这个集合包括反斜杠(\),括号(()),和号(&),单引号('),破折号(-),点(.),空格( ),所有的数字(0-9),大写字母(A-Z)和小写字母(a-z)。 /// +:这表示前面的字符集可以在字符串中出现一次或多次。 /// 所以,整个表达式的意思是:匹配一次或多次的上述字符集中的字符,并将匹配的部分作为名为 "title_number_en" 的捕获组 -fn regex_item_title_number_en() -> &'static str { - r"(?P[\\()&'-. 0-9A-Za-z]+)" +fn regex_item_title_number_en() -> String { + return regex_item_multi(KEY_TITLE_NUMBER_EN, 0, ®EX_RULES.title_number_en); } /// 匹配标题年 -fn regex_item_title_year() -> &'static str { - r"(?P\d{4})" +fn regex_item_title_year() -> String { + return regex_item_multi_str(KEY_TITLE_YEAR, 0, &vec!["\\d{4}"]); } /// 匹配英文季名 /// https://docs.rs/regex/latest/regex/ -/// Strange.Love -/// 1+1 -/// Making.of -/// Tape.1.Side.A -/// Tape.Side.A.1 -/// 这是一个正则表达式,用于匹配一种特定的字符串模式。下面是各部分的解释: -/// (?P...):这是一个命名捕获组,名为 "episode_title_en"。捕获组可以用来从匹配的文本中提取出特定部分。 -/// [&-. A-Za-z]+[0-9]{1,4}[&-. A-Za-z]+:这个模式匹配一次或多次的字符集中的字符(包括&,-,.,空格,以及所有的大写和小写字母),后面跟着1到4个数字,再后面跟着一次或多次的字符集中的字符。 -/// [&-. A-Za-z]+[0-9]{1}:这个模式匹配一次或多次的字符集中的字符,后面跟着一个数字。 -//// [&-. A-Za-z]+:这个模式匹配一次或多次的字符集中的字符。 -/// [0-9]+\+[0-9]+:这个模式匹配一次或多次的数字,后面跟着一个加号,再后面跟着一次或多次的数字。 -/// |:这是或(or)操作符,表示匹配前面或后面的模式。 -/// 所以,整个表达式的意思是:匹配四种可能的模式之一,并将匹配的部分作为名为 "episode_title_en" 的捕获组 -fn regex_item_episode_title_en() -> &'static str { - r"(?P[&-. A-Za-z]+[0-9]{1,4}[&-. A-Za-z]+|[&-. A-Za-z]+[0-9]{1}|[&-. A-Za-z]+|[0-9]+\+[0-9]+)" +// 这是一个正则表达式,用于匹配一种特定的字符串模式。下面是各部分的解释: +// (?P...):这是一个命名捕获组,名为 "episode_title_en"。捕获组可以用来从匹配的文本中提取出特定部分。 +// [&-. A-Za-z]+[0-9]{1,4}[&-. A-Za-z]+:这个模式匹配一次或多次的字符集中的字符(包括&,-,.,空格,以及所有的大写和小写字母),后面跟着1到4个数字,再后面跟着一次或多次的字符集中的字符。 +// [&-. A-Za-z]+[0-9]{1,3}:这个模式匹配一次或多次的字符集中的字符,后面跟着1到3个数字。 +// [&-. A-Za-z]+:这个模式匹配一次或多次的字符集中的字符。 +// [0-9]+\+[0-9]+:这个模式匹配一次或多次的数字,后面跟着一个加号,再后面跟着一次或多次的数字。 +// [0-9]{1,3}[&-. A-Za-z]+:这个模式匹配1到3个数字,后面跟着一次或多次的字符集中的字符。 +// |:这是或(or)操作符,表示匹配前面或后面的模式。 +// 所以,整个表达式的意思是:匹配五种可能的模式之一,并将匹配的部分作为名为 "episode_title_en" 的捕获组。 +fn regex_item_episode_title_en() -> String { + return regex_item_multi(KEY_EPISODE_TITLE_EN, 0, ®EX_RULES.episode_title_en); } /// 匹配中文名 -fn regex_item_episode_title_cn() -> &'static str { - r"(?P[:\p{Script=Han}\p{Script=Hiragana}]+)" +fn regex_item_episode_title_cn() -> String { + return regex_item_multi(KEY_EPISODE_TITLE_CN, 0, ®EX_RULES.episode_title_cn); +} + +/// 匹配日文 +fn regex_item_episode_title_jp() -> String { + return regex_item_multi(KEY_EPISODE_TITLE_JP, 0, ®EX_RULES.episode_title_jp); +} + +/// 匹配中文名 +fn regex_item_text_cn() -> String { + return regex_item_multi_str(KEY_TEXT_CN, 0, &vec!["\\p{Script=Han}+"]); } /// 匹配年 2023 -fn regex_item_year() -> &'static str { - r"(?P\d{4})" +fn regex_item_year() -> String { + return regex_item_multi_str(KEY_YEAR, 0, &vec!["\\d{4}"]); } /// 匹配年 2021-2023 -fn regex_item_year_start_to_end() -> &'static str { - r"(?P\d{4}-\d{4})" +fn regex_item_year_start_to_end() -> String { + return regex_item_multi_str(KEY_YEAR_START_TO_END, 0, &vec!["\\d{4}-\\d{4}"]); } /// 匹配 2023-12-01 -fn regex_item_year_month_day() -> &'static str { - r"(?P\d{4}\.\d{2}\.\d{2})" +fn regex_item_year_month_day() -> String { + return regex_item_multi_str(KEY_YEAR_MONTH_DAY, 0, &vec!["\\d{4}\\.\\d{2}\\.\\d{2}"]); } /// 匹配季和集 S01E01 -fn regex_item_season_episode() -> &'static str { - r"(?PS\d{1,2}E\d{2})" +fn regex_item_season_episode() -> String { + return regex_item_multi_str(KEY_SEASON_EPISODE, 0, &vec!["[Ss]\\d{1,2}\\.?[Ee]\\d{2,3}"]); } /// 匹配季和集 S01E01E02 -fn regex_item_season_episode_episode() -> &'static str { - r"(?PS\d{2}E\d{2}E\d{2})" +fn regex_item_season_episode_episode() -> String { + return regex_item_multi_str(KEY_SEASON_EPISODE_EPISODE, 0, &vec!["S\\d{2}E\\d{2,3}E\\d{2,3}"]); } /// 匹配季和集 S01E01 fn regex_item_season_episode_split() -> &'static str { - r"(?PS\d{1,2})(?PE[Pp]?\d{2})" + r"(?PS\d{1,2})\.?(?PE[Pp]?\d{2,3})" +} + +fn regex_item_season_and_episode() -> &'static str { + r"(?PS\d{1,2})\.(?PE[Pp]?\d{2,3})" } /// 匹配季和集 S01E01E02 fn regex_item_season_episode_episode_split() -> &'static str { - r"(?PS\d{2})(?PE[Pp]?\d{2})(?PE[Pp]?\d{2})" + r"(?PS\d{2})(?PE[Pp]?\d{2,3})(?PE[Pp]?\d{2,3})" } -/// match episode E01 EP01 Ep01 +/// 匹配单个数字 +fn regex_item_season_number() -> String { + return regex_item_multi_str(KEY_SEASON_NUMBER, 0, &vec!["[0-9]{1}"]); +} + +/// match episode E01 EP01 Ep01 E100 E1024 /// 也可以匹配 S01E01中的E01,所以解析逻辑需要放在regex_item_season_episode之后 -fn regex_item_episode() -> &'static str { - r"(?PE[Pp]?\d{2})" +fn regex_item_episode() -> String { + return regex_item_multi_str(KEY_EPISODE, 0, &vec!["E[Pp]?\\d{2,4}"]); } /// 匹配两位数字 -fn regex_item_episode_number() -> &'static str { - r"(?P\d{2})" +fn regex_item_episode_number() -> String { + return regex_item_multi_str(KEY_EPISODE_NUMBER, 0, &vec!["\\d{2,3}"]); } /// match season S01 -fn regex_item_season() -> &'static str { - r"(?PS\d{2})" +fn regex_item_season() -> String { + return regex_item_multi_str(KEY_SEASON, 0, &vec!["S\\d{2}"]); } /// match 第01季 -fn regex_item_season_cn() -> &'static str { - r"(?P第\d{2}季)" +fn regex_item_season_cn() -> String { + return regex_item_multi_str(KEY_SEASON_CN, 0, &vec!["第\\d{1,2}季"]); +} + +/// match 第01集 +fn regex_item_episode_cn() -> String { + return regex_item_multi_str(KEY_EPISODE_CN, 0, &vec!["第\\d{1,2}[集|话]"]); } /// 全十一季 -fn regex_item_season_all_cn() -> &'static str { - r"(?P全[一二三四五六七八九十]+季)" +fn regex_item_season_all_cn() -> String { + return regex_item_multi_str(KEY_SEASON_ALL_CN, 0, &vec!["全[一二三四五六七八九十]+季"]); } /// 第01-08季 -fn regex_item_season_start_to_end_cn() -> &'static str { - r"(?P第\d{2}-\d{2}季)" +fn regex_item_season_start_to_end_cn() -> String { + return regex_item_multi_str(KEY_SEASON_START_TO_END_CN, 0, &vec!["第\\d{2}-\\d{2}季"]); } /// S01-S03 -fn regex_item_season_start_to_end_en() -> &'static str { - r"(?PS\d{2}-S\d{2})" +fn regex_item_season_start_to_end_en() -> String { + return regex_item_multi_str(KEY_SEASON_START_TO_END_EN, 0, &vec!["S\\d{2}-S\\d{2}"]); } /// 匹配分辨率 1080P/1080p/1080i -fn regex_item_resolution() -> &'static str { - r"(?P240[Ppi]|360[Ppi]|480[Ppi]|720[Ppi]|1080[Ppi]|1440[Ppi]|2160[Ppi]|4320[Ppi]|4[Kk])" +fn regex_item_resolution_cn() -> String { + return regex_item_multi(KEY_RESOLUTION_CN, 0, ®EX_RULES.resolution_cn); } /// 版本 /// [三体].Three-Body.2023.S01E01.2160p.V3.WEB-DL.HEVC.DDP5.1.Atmos.&.AAC-QHstudIo.mp4 -fn regex_item_version() -> &'static str { - r"(?PV\d{1})" +fn regex_item_version() -> String { + return regex_item_multi(KEY_VERSION, 0, ®EX_RULES.version); } -/// 匹配国家 -fn regex_item_country() -> &'static str { - r"(?PUS|JP|Ger|GER|USA)" +/// 匹配字幕语言 +fn regex_item_subtitle_en() -> String { + return regex_item_subtitle_en_multi(0); } -/// 匹配字幕语言 -fn regex_item_subtitle_en() -> &'static str { - r"(?PSweSub)" +fn regex_item_subtitle_en_multi(index: i32) -> String { + return regex_item_multi(KEY_SUBTITLE_EN, index, ®EX_RULES.subtitle_en); } /// 匹配中文名 -fn regex_item_subtitle_cn() -> &'static str { - r"(?P中[英|美|日|挪]字幕)" +fn regex_item_subtitle_cn() -> String { + return regex_item_multi_str(KEY_SUBTITLE_CN, 0, &vec!["中[英|美|日|挪]?字[幕]?(简繁双语特效)?"]); +} + +/// 匹配中文名 +fn regex_item_audio_cn() -> String { + return regex_item_multi_str(KEY_AUDIO_CN, 0, &vec!["中[英|美|日|挪]双语"]); } /// 匹配特典 -fn regex_item_special() -> &'static str { - r"(?PManual)" +fn regex_item_special() -> String { + return regex_item_multi_str(KEY_SPECIAL, 0, &vec!["Manual"]); +} + +/// +fn regex_item_anything() -> String { + return regex_item_multi_str(KEY_ANYTHING, 0, &vec![".*"]); +} + +/// 匹配国家 +fn regex_item_country() -> String { + return regex_item_country_multi(0); +} + +fn regex_item_country_multi(index: i32) -> String { + return regex_item_multi(KEY_COUNTRY, index, ®EX_RULES.country); +} + +/// 匹配分辨率 1080P/1080p/1080i +fn regex_item_resolution() -> String { + return regex_item_resolution_multi(0); +} + +fn regex_item_resolution_multi(index: i32) -> String { + return regex_item_multi(KEY_RESOLUTION, index, ®EX_RULES.resolution); } /// 匹配视频源 @@ -291,58 +392,26 @@ fn regex_item_special() -> &'static str { /// - **REMUX:** 是重新复制的缩写,表示视频和音频流从一个容器格式复制到另一个容器格式,而不进行重新编码。在这里,它指的是将视频从一个源中提取并重新封装,而不对视频进行再压缩,以保持最佳的视觉和听觉质量。 /// 因此,AVC.REMUX 文件通常是高质量的视频文件,因为它们没有经历再压缩,保留了原始来源的视频和音频质量。 fn regex_item_source() -> String { - let mut combined = String::from("(?P("); - let last_index = REGEX_RULES.source.len() - 1; - for (index, item) in REGEX_RULES.source.iter().enumerate() { - combined.push_str(item); - if index != (last_index) { - combined.push_str("|") - } - } - combined.push_str("))"); - return combined; + return regex_item_source_multi(0); } fn regex_item_source_multi(index: i32) -> String { - let mut combined = if index == 0 { String::from("(?P(") } else { String::from(format!("(?P(", index)) }; - let last_index = REGEX_RULES.source.len() - 1; - for (index, item) in REGEX_RULES.source.iter().enumerate() { - combined.push_str(item); - if index != (last_index) { - combined.push_str("|") - } - } - combined.push_str("))"); - return combined; + return regex_item_multi(KEY_SOURCE, index, ®EX_RULES.source); } /// 匹配视频源厂商 /// netflix = nf fn regex_item_company() -> String { - let mut combined = String::from("(?P("); - let last_index = REGEX_RULES.company.len() - 1; - for (index, item) in REGEX_RULES.company.iter().enumerate() { - combined.push_str(item); - if index != (last_index) { - combined.push_str("|") - } - } - combined.push_str("))"); - return combined; + return regex_item_multi(KEY_COMPANY, 0, ®EX_RULES.company); } /// 匹配视频编码器 fn regex_item_video_codec() -> String { - let mut combined = String::from("(?P("); - let last_index = REGEX_RULES.video_codec.len() - 1; - for (index, item) in REGEX_RULES.video_codec.iter().enumerate() { - combined.push_str(item); - if index != (last_index) { - combined.push_str("|") - } - } - combined.push_str("))"); - return combined; + return regex_item_video_codec_multi(0); +} + +fn regex_item_video_codec_multi(index: i32) -> String { + return regex_item_multi(KEY_VIDEO_CODEC, index, ®EX_RULES.video_codec); } /// 视频编码器扩展信息 @@ -350,68 +419,45 @@ fn regex_item_video_codec() -> String { /// color depth /// FPS fn regex_item_color() -> String { - let mut combined = String::from("(?P("); - let last_index = REGEX_RULES.color.len() - 1; - for (index, item) in REGEX_RULES.color.iter().enumerate() { - combined.push_str(item); - if index != (last_index) { - combined.push_str("|") - } - } - combined.push_str("))"); - return combined; + return regex_item_color_multi(0); } fn regex_item_color_multi(index: i32) -> String { - let mut combined = if index == 0 { String::from("(?P(") } else { String::from(format!("(?P(", index)) }; - let last_index = REGEX_RULES.color.len() - 1; - for (index, item) in REGEX_RULES.color.iter().enumerate() { - combined.push_str(item); - if index != (last_index) { - combined.push_str("|") - } - } - combined.push_str("))"); - return combined; + return regex_item_multi(KEY_COLOR, index, ®EX_RULES.color); +} + +fn regex_item_audio_codec_multi(index: i32) -> String { + return regex_item_multi(KEY_AUDIO_CODEC, index, ®EX_RULES.audio_codec); +} + +fn regex_item_edition() -> String { + return regex_item_multi(KEY_EDITION, 0, ®EX_RULES.edition); } /// 匹配音频编码器 fn regex_item_audio_codec() -> String { - let mut combined = String::from("(?P("); - let last_index = REGEX_RULES.audio_codec.len() - 1; - for (index, item) in REGEX_RULES.audio_codec.iter().enumerate() { - combined.push_str(item); - if index != (last_index) { - combined.push_str("|") - } - } - combined.push_str("))"); - return combined; + return regex_item_audio_codec_multi(0); } /// 匹配发布组 fn regex_item_release_group() -> String { - let mut combined = String::from("(?P("); - let last_index = REGEX_RULES.release_group.len() - 1; - for (index, item) in REGEX_RULES.release_group.iter().enumerate() { - combined.push_str(item); - if index != (last_index) { - combined.push_str("|") - } - } - combined.push_str("))"); - return combined; + return regex_item_multi(KEY_RELEASE_GROUP, 0, ®EX_RULES.release_group); } /// 视频格式 -fn regex_item_format() -> &'static str { - r"(?P(mp4|mkv|ts|iso|rmvb|avi|mov|mepg|mpg|wmv|3gp|asf|m4v|flv|m2ts))" +fn regex_item_extension() -> String { + return regex_item_multi_str(KEY_EXTENSION, 0, &vec!["mp4|mkv|iso|rmvb|avi|mov|mepg|mpg|wmv|3gp|asf|m4v|flv"]); } /// 匹配数字字母混合 /// 例如:4ABDCA70 -fn regex_item_mix_numbers_letters() -> &'static str { - r"(?P[0-9A-Za-z]+)" +fn regex_item_mix_numbers_letters() -> String { + return regex_item_multi_str(KEY_MIX_NUMBERS_LETTERS, 0, &vec!["[\\.0-9A-Za-z]+"]); +} + +/// 匹配单个数字 +fn regex_item_number() -> String { + return regex_item_multi_str(KEY_NUMBER, 0, &vec!["[0-9]{1}"]); } /// @@ -427,56 +473,49 @@ fn regex_build(rule: &Rule, regex_index: &mut HashMap) -> String { // build core let mut format = before; - format = format.replace("$title_cn$", regex_item_title_cn()); - format = format.replace("$title_number_cn$", regex_item_title_number_cn()); - format = format.replace("$title_en$", regex_item_title_en()); - format = format.replace("$title_number_en$", regex_item_title_number_en()); - format = format.replace("$title_year$", regex_item_title_year()); - format = format.replace("$year_start_to_end$", regex_item_year_start_to_end()); - format = format.replace("$year_month_day$", regex_item_year_month_day()); - format = format.replace("$year$", regex_item_year()); - format = format.replace("$season_episode$", regex_item_season_episode()); - format = format.replace("$season_episode_episode$", regex_item_season_episode_episode()); - format = format.replace("$season$", regex_item_season()); - format = format.replace("$season_start_to_end_cn$", regex_item_season_start_to_end_cn()); - format = format.replace("$season_all_cn$", regex_item_season_all_cn()); - format = format.replace("$season_cn$", regex_item_season_cn()); - format = format.replace("$season_start_to_end_en$", regex_item_season_start_to_end_en()); - format = format.replace("$episode$", regex_item_episode()); - format = format.replace("$episode_number$", regex_item_episode_number()); - format = format.replace("$episode_title_en$", regex_item_episode_title_en()); - format = format.replace("$episode_title_cn$", regex_item_episode_title_cn()); - format = format.replace("$resolution$", regex_item_resolution()); - format = format.replace("$version$", regex_item_version()); - - // - let mut source_index = 0; - while let Some(_index) = format.find("$source$") { - format = format.replacen("$source$", ®ex_item_source_multi(source_index), 1); - source_index += 1; - } - regex_index.insert("$source$".to_string(), source_index); - - format = format.replace("$company$", ®ex_item_company()); - format = format.replace("$video_codec$", ®ex_item_video_codec()); - - // - let mut color_index = 0; - while let Some(_index) = format.find("$color$") { - format = format.replacen("$color$", ®ex_item_color_multi(color_index), 1); - color_index += 1; - } - regex_index.insert("$color$".to_string(), color_index); - - // - format = format.replace("$audio_codec$", ®ex_item_audio_codec()); - format = format.replace("$release_group$", ®ex_item_release_group()); - format = format.replace("$subtitle_en$", regex_item_subtitle_en()); - format = format.replace("$subtitle_cn$", regex_item_subtitle_cn()); - format = format.replace("$special$", regex_item_special()); - format = format.replace("$country$", regex_item_country()); - format = format.replace("$extension$", ®ex_item_format()); - format = format.replace("$mix_numbers_letters$", ®ex_item_mix_numbers_letters()); + format = format.replace(&wrap(KEY_TITLE_CN), ®ex_item_title_cn()); + format = format.replace(&wrap(KEY_TITLE_NUMBER_CN), ®ex_item_title_number_cn()); + format = format.replace(&wrap(KEY_TITLE_EN), ®ex_item_title_en()); + format = format.replace(&wrap(KEY_TITLE_NUMBER_EN), ®ex_item_title_number_en()); + format = format.replace(&wrap(KEY_TITLE_YEAR), ®ex_item_title_year()); + format = format.replace(&wrap(KEY_YEAR_START_TO_END), ®ex_item_year_start_to_end()); + format = format.replace(&wrap(KEY_YEAR_MONTH_DAY), ®ex_item_year_month_day()); + format = format.replace(&wrap(KEY_YEAR), ®ex_item_year()); + format = format.replace(&wrap(KEY_SEASON_EPISODE), ®ex_item_season_episode()); + format = format.replace(&wrap(KEY_SEASON_EPISODE_EPISODE), ®ex_item_season_episode_episode()); + format = format.replace(&wrap(KEY_SEASON), ®ex_item_season()); + format = format.replace(&wrap(KEY_SEASON_START_TO_END_CN), ®ex_item_season_start_to_end_cn()); + format = format.replace(&wrap(KEY_SEASON_ALL_CN), ®ex_item_season_all_cn()); + format = format.replace(&wrap(KEY_SEASON_CN), ®ex_item_season_cn()); + format = format.replace(&wrap(KEY_SEASON_START_TO_END_EN), ®ex_item_season_start_to_end_en()); + format = format.replace(&wrap(KEY_EPISODE), ®ex_item_episode()); + format = format.replace(&wrap(KEY_EPISODE_CN), ®ex_item_episode_cn()); + format = format.replace(&wrap(KEY_EPISODE_NUMBER), ®ex_item_episode_number()); + format = format.replace(&wrap(KEY_EPISODE_TITLE_EN), ®ex_item_episode_title_en()); + format = format.replace(&wrap(KEY_EPISODE_TITLE_CN), ®ex_item_episode_title_cn()); + format = format.replace(&wrap(KEY_EPISODE_TITLE_JP), ®ex_item_episode_title_jp()); + format = format.replace(&wrap(KEY_RESOLUTION_CN), ®ex_item_resolution_cn()); + format = format.replace(&wrap(KEY_VERSION), ®ex_item_version()); + format = format.replace(&wrap(KEY_COMPANY), ®ex_item_company()); + format = format.replace(&wrap(KEY_SEASON_TITLE_CN), ®ex_item_season_title_cn()); + format = format.replace(&wrap(KEY_RELEASE_GROUP), ®ex_item_release_group()); + format = format.replace(&wrap(KEY_SUBTITLE_CN), ®ex_item_subtitle_cn()); + format = format.replace(&wrap(KEY_AUDIO_CN), ®ex_item_audio_cn()); + format = format.replace(&wrap(KEY_SPECIAL), ®ex_item_special()); + format = format.replace(&wrap(KEY_EXTENSION), ®ex_item_extension()); + format = format.replace(&wrap(KEY_MIX_NUMBERS_LETTERS), ®ex_item_mix_numbers_letters()); + format = format.replace(&wrap(KEY_NUMBER), ®ex_item_number()); + format = format.replace(&wrap(KEY_SEASON_NUMBER), ®ex_item_season_number()); + format = format.replace(&wrap(KEY_TEXT_CN), ®ex_item_text_cn()); + format = format.replace(&wrap(KEY_ANYTHING), ®ex_item_anything()); + + format = replace_multi(format, &wrap(KEY_SUBTITLE_EN), |index: i32| regex_item_subtitle_en_multi(index)); + format = replace_multi(format, &wrap(KEY_COUNTRY), |index: i32| regex_item_country_multi(index)); + format = replace_multi(format, &wrap(KEY_SOURCE), |index: i32| regex_item_source_multi(index)); + format = replace_multi(format, &wrap(KEY_VIDEO_CODEC), |index: i32| regex_item_video_codec_multi(index)); + format = replace_multi(format, &wrap(KEY_AUDIO_CODEC), |index: i32| regex_item_audio_codec_multi(index)); + format = replace_multi(format, &wrap(KEY_COLOR), |index: i32| regex_item_color_multi(index)); + format = replace_multi(format, &wrap(KEY_RESOLUTION), |index: i32| regex_item_resolution_multi(index)); // build after let after = regex_builder_after(format); @@ -488,18 +527,39 @@ fn regex_build(rule: &Rule, regex_index: &mut HashMap) -> String { return ret; } -fn regex_builder_pre(regex_config: String) -> String { +fn replace_multi(format: String, arg: &str, exec: impl Fn(i32) -> String) -> String { + let mut index = 0; + let mut ret = format.clone(); + while let Some(_index) = ret.find(arg) { + let target = exec(index); + ret = ret.replacen(arg, &target, 1); + index += 1; + } + return ret; +} + +fn regex_builder_pre(config: String) -> String { // replace special chars - let config = regex_config.replace("[", "\\["); + // 对于强匹配的规则,需要将一些特殊字符转义,例如.()[]等,他们并不是正则表达式的一部分 + // { + // "example": "[三国].Three.Kingdoms.EP01.2010.BluRay.720p.x264.AC3-CMCT.mkv", + // "rule": "[$title_cn$].$title_en$.$episode$.$year$.$source$.$resolution$.$video_codec$.$audio_codec$.$extension$" + // } + // 例如这里面的[$title_cn$]的[和]需要转义,否则会被当做正则表达式的一部分 + let config = config.replace("[", "\\["); let config = config.replace("]", "\\]"); let config = config.replace("(", "\\("); let config = config.replace(")", "\\)"); + // tracing::debug!("regex_builder_pre config = {}", config); return config; } -fn regex_builder_after(config: String) -> String { - return config; +fn regex_builder_after(input: String) -> String { + // 最后拼接出来的正则表达式是有规律的,需要将.转义 + let input = input.replace(").(", ")\\.("); + // tracing::debug!("regex_builder_after input = {}", input); + return input; } fn regex_builder_before(_rule: &Rule, pre: String) -> String { @@ -507,165 +567,196 @@ fn regex_builder_before(_rule: &Rule, pre: String) -> String { } /// build token from input by config -fn build_tokens(rule: &Rule, input: &str) -> Option { +fn build_tokens(rule: &Rule, input: &str) -> Option { + // tracing::debug!("build_tokens input = {}", input); + + let new_input = input_before(rule, input); + + // tracing::debug!("build_tokens new_input = {}", new_input); + let mut regex_index: HashMap = HashMap::new(); let final_regex = regex_build(rule, &mut regex_index); let regex = Regex::new(final_regex.as_str()).unwrap(); - let new_input = input_before(rule, input); - if let Some(captures) = regex.captures(&new_input) { - let mut tokenizer = Tokens { tokens: HashMap::new() }; + let mut token = Token { tokens: HashMap::new() }; + + tracing::debug!("build_tokens new_input = {}", new_input); + tracing::debug!("build_tokens regex = {}", rule.rule); + tracing::debug!("build_tokens final_regex = {}", final_regex); - tokenizer.tokens.insert("origin_title".to_string(), input.to_string()); + token.tokens.insert(KEY_ORIGIN_TITLE.to_string(), input.to_string()); - if let Some(value) = captures.name("title_cn") { - tokenizer.tokens.insert("title_cn".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_TITLE_CN) { + token.tokens.insert(KEY_TITLE_CN.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("title_number_cn") { - tokenizer.tokens.insert("title_cn".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_TITLE_NUMBER_CN) { + token.tokens.insert(KEY_TITLE_CN.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("title_en") { + if let Some(value) = captures.name(KEY_TITLE_EN) { let value = value.as_str().to_string(); - // 替换掉英文中的连接字符 + let value = value.replace("∙", " ").trim_end().to_string(); let value = value.replace(".", " ").trim_end().to_string(); let value = value.replace("-", " ").trim_end().to_string(); - tokenizer.tokens.insert("title_en".to_string(), value); + let value = value.trim_start().to_string(); + let aka: Vec<&str> = value.split(KEY_AKA).collect(); + if aka.len() == 2 { + token.tokens.insert(KEY_AKA_TITLE_EN.to_string(), value.to_string()); + token.tokens.insert(KEY_AKA_TITLE_EN_FIRST.to_string(), aka[0].trim_end().to_string()); + token.tokens.insert(KEY_AKA_TITLE_EN_SECOND.to_string(), aka[1].trim_start().to_string()); + + // 使用aka的第一部分作为默认名字 + token.tokens.insert(KEY_TITLE_EN.to_string(), aka[0].trim_end().to_string()); + } else { + // 原始名字 + token.tokens.insert(KEY_TITLE_EN.to_string(), value); + } } - if let Some(value) = captures.name("title_year") { - tokenizer.tokens.insert("title_year".to_string(), value.as_str().to_string()); - tokenizer.tokens.insert("title_en".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_TITLE_YEAR) { + token.tokens.insert(KEY_TITLE_YEAR.to_string(), value.as_str().to_string()); + token.tokens.insert(KEY_TITLE_EN.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("title_number_en") { + if let Some(value) = captures.name(KEY_TITLE_NUMBER_EN) { let value = value.as_str().to_string(); - tokenizer.tokens.insert("title_en".to_string(), value); - } - - if let Some(value) = captures.name("year") { - tokenizer.tokens.insert("year".to_string(), value.as_str().to_string()); + let value = value.replace(".", " ").trim_end().to_string(); + let value = value.replace("-", " ").trim_end().to_string(); + token.tokens.insert(KEY_TITLE_EN.to_string(), value); } - if let Some(value) = captures.name("year_start_to_end") { - tokenizer.tokens.insert("year_start_to_end".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_YEAR) { + token.tokens.insert(KEY_YEAR.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("year_month_day") { - tokenizer.tokens.insert("year_month_day".to_string(), value.as_str().to_string()); - } - - if let Some(value) = captures.name("season_episode") { - let value = value.as_str().to_string(); - tokenizer.tokens.insert("season_episode".to_string(), value.clone()); + if let Some(value) = captures.name(KEY_SEASON_EPISODE) { + let value = value.as_str().to_string().to_uppercase(); + token.tokens.insert(KEY_SEASON_EPISODE.to_string(), value.clone()); if let Ok(regex) = Regex::new(®ex_item_season_episode_split()) { if let Some(captures) = regex.captures(&value) { - if let Some(value) = captures.name("season") { - tokenizer.tokens.insert("season".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SEASON) { + token.tokens.insert(KEY_SEASON.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("episode") { - tokenizer.tokens.insert("episode".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_EPISODE) { + token.tokens.insert(KEY_EPISODE.to_string(), value.as_str().to_string()); } } } } - if let Some(value) = captures.name("season_episode_episode") { + if let Some(value) = captures.name(KEY_SEASON_EPISODE_EPISODE) { let value = value.as_str().to_string(); - tokenizer.tokens.insert("season_episode_episode".to_string(), value.clone()); + token.tokens.insert(KEY_SEASON_EPISODE_EPISODE.to_string(), value.clone()); if let Ok(regex) = Regex::new(®ex_item_season_episode_episode_split()) { if let Some(captures) = regex.captures(&value) { - if let Some(value) = captures.name("season") { - tokenizer.tokens.insert("season".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SEASON) { + token.tokens.insert(KEY_SEASON.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("episode1") { - tokenizer.tokens.insert("episode".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_EPISODE_1) { + token.tokens.insert(KEY_EPISODE.to_string(), value.as_str().to_string()); + token.tokens.insert(KEY_EPISODE_1.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("episode2") { - tokenizer.tokens.insert("episode2".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_EPISODE_2) { + token.tokens.insert(KEY_EPISODE_2.to_string(), value.as_str().to_string()); } } } } - if let Some(value) = captures.name("season") { - tokenizer.tokens.insert("season".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SEASON) { + token.tokens.insert(KEY_SEASON.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("season_cn") { - tokenizer.tokens.insert("season_cn".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SEASON_ALL_CN) { + token.tokens.insert(KEY_SEASON_ALL_CN.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("season_all_cn") { - tokenizer.tokens.insert("season_all_cn".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SEASON_START_TO_END_CN) { + token.tokens.insert(KEY_SEASON_START_TO_END_CN.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("season_start_to_end_cn") { - tokenizer.tokens.insert("season_start_to_end_cn".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SEASON_START_TO_END_EN) { + token.tokens.insert(KEY_SEASON_START_TO_END_EN.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("season_start_to_end_en") { - tokenizer.tokens.insert("season_start_to_end_en".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_EPISODE) { + let value = value.as_str().to_string(); + let value = value.replace("Ep", "E"); + let value = value.replace("EP", "E"); + token.tokens.insert(KEY_EPISODE.to_string(), value); } - if let Some(value) = captures.name("episode") { - tokenizer.tokens.insert("episode".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_EPISODE_CN) { + let value = value.as_str().to_string(); + let value = value.replace("第", ""); + let value = value.replace("集", ""); + let value = value.replace("话", ""); + token.tokens.insert(KEY_EPISODE.to_string(), format!("E{}", value)); } - if let Some(value) = captures.name("episode_number") { - tokenizer.tokens.insert("episode".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_EPISODE_NUMBER) { + tracing::debug!("episode_number = {}", value.as_str().to_string()); + // value to i32 + let value = value.as_str().to_string().parse::().unwrap(); + token.tokens.insert(KEY_EPISODE.to_string(), format!("E{:02}", value)); } - if let Some(value) = captures.name("resolution") { + if let Some(value) = captures.name(KEY_RESOLUTION) { let value = value.as_str().to_lowercase().to_string(); let value = value.replace("4k", "2160p"); let value = value.replace("2k", "2160p"); - tokenizer.tokens.insert("resolution".to_string(), value); + let value = value.replace("1920x1080", "1080p"); + let value = value.replace("1920X1080", "1080p"); + let value = value.replace("3840x2160", "2160p"); + let value = value.replace("3840X2160", "2160p"); + token.tokens.insert(KEY_RESOLUTION.to_string(), value); } - if let Some(value) = captures.name("source") { + if let Some(value) = captures.name(KEY_SOURCE) { let mut source = value.as_str().to_string(); - if let Some(index) = regex_index.get("$source$") { - if *index > 0 { - // 处理扩展源 - if let Some(value) = captures.name("source1") { - let source1 = value.as_str().to_string(); - // 合并源 - source = format!("{}.{}", source, source1); - tokenizer.tokens.insert("source1".to_string(), source1); - } - } - } - // 处理BluRay的各种样式 - let bluray_regex = Regex::new(r"Blu[-]?[Rr]ay").unwrap(); + let bluray_regex = Regex::new(r"[Bb]lu[-]?[Rr]ay").unwrap(); source = bluray_regex.replace(&source, "BluRay").to_string(); // 处理Remux的各种样式 source = source.replace("REMUX", "Remux").to_string(); + source = source.replace("ReMuX", "Remux").to_string(); + + // + source = source.replace("Remux BluRay", "BluRay.Remux").to_string(); // 处理BluRay[.- ]Remux的各种样式 - let all_regex = Regex::new(r"BluRay[ -]Remux").unwrap(); - source = all_regex.replace(&source, "BluRay.Remux").to_string(); + source = Regex::new(r"BluRay[ -]Remux").unwrap().replace(&source, "BluRay.Remux").to_string(); // - tokenizer.tokens.insert("source".to_string(), source); + if source == "WEB" { + source = source.replace("WEB", "WEB-DL").to_string(); + } + source = source.replace("web", "WEB-DL").to_string(); + source = source.replace("WEB DL", "WEB-DL").to_string(); + + // BDRip + source = source.replace("BDRIP", "BDRip").to_string(); + source = source.replace("BDrip", "BDRip").to_string(); + + // + token.tokens.insert(KEY_SOURCE.to_string(), source); } - if let Some(value) = captures.name("company") { - tokenizer.tokens.insert("company".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_COMPANY) { + token.tokens.insert(KEY_COMPANY.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("video_codec") { + if let Some(value) = captures.name(KEY_VIDEO_CODEC) { let mut video_codec = value.as_str().to_string(); // @@ -681,104 +772,332 @@ fn build_tokens(rule: &Rule, input: &str) -> Option { video_codec = regex.replace(&video_codec, "H.265").to_string(); // + video_codec = video_codec.replace("AVC.REMUX", "H.264").to_string(); video_codec = video_codec.replace("AVC", "H.264").to_string(); // video_codec = video_codec.replace("HEVC", "H.265").to_string(); - tokenizer.tokens.insert("video_codec".to_string(), video_codec); + token.tokens.insert(KEY_VIDEO_CODEC.to_string(), video_codec); } - if let Some(index) = regex_index.get("$color$") { - for i in 0..(*index) { - if i == 0 { - if let Some(value) = captures.name("color") { - tokenizer.tokens.insert("color".to_string(), value.as_str().to_string()); - } - } else { - let target = format!("color{}", index); - if let Some(value) = captures.name(&target) { - tokenizer.tokens.insert(target, value.as_str().to_string()); - } - } - } + if let Some(value) = captures.name(KEY_COLOR) { + token.tokens.insert(KEY_COLOR.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("audio_codec") { - let mut audio_codec = value.as_str().to_string(); + if let Some(value) = captures.name(KEY_AUDIO_CODEC) { + let mut value = value.as_str().to_string(); // 处理audio和atmos大小写 - audio_codec = audio_codec.replace("audio", "Audio").to_string(); - audio_codec = audio_codec.replace("atmos", "Atmos").to_string(); + value = value.replace("audio", "Audio").to_string(); + value = value.replace("atmos", "Atmos").to_string(); // 处理DTS-HD的各种组合 - audio_codec = audio_codec.replace(" MA ", ".MA.").to_string(); - audio_codec = audio_codec.replace(".MA5", ".MA.5").to_string(); - audio_codec = audio_codec.replace(".MA2", ".MA.2").to_string(); - audio_codec = audio_codec.replace(".MA7", ".MA.7").to_string(); - audio_codec = audio_codec.replace("DMa5", "D.MA.5").to_string(); - audio_codec = audio_codec.replace("DMa7", "D.MA.7").to_string(); - audio_codec = audio_codec.replace("DMa2", "D.MA.2").to_string(); - audio_codec = audio_codec.replace("Dts", "DTS").to_string(); - audio_codec = audio_codec.replace("HD ", "HD.").to_string(); + value = value.replace(" MA ", ".MA.").to_string(); + value = value.replace(".MA5", ".MA.5").to_string(); + value = value.replace(".MA2", ".MA.2").to_string(); + value = value.replace(".MA7", ".MA.7").to_string(); + value = value.replace("DMa5", "D.MA.5").to_string(); + value = value.replace("DMa7", "D.MA.7").to_string(); + value = value.replace("DMa2", "D.MA.2").to_string(); + value = value.replace("Dts", "DTS").to_string(); + value = value.replace("HD ", "HD.").to_string(); // DDP - audio_codec = audio_codec.replace("DDP2.0", "DDP.2.0").to_string(); - audio_codec = audio_codec.replace("DDP5.1", "DDP.5.1").to_string(); - audio_codec = audio_codec.replace("DDP7.1", "DDP.7.1").to_string(); + value = value.replace("DDP2.0", "DDP.2.0").to_string(); + value = value.replace("DDP5.1", "DDP.5.1").to_string(); + value = value.replace("DDP7.1", "DDP.7.1").to_string(); // - audio_codec = audio_codec.replace(".&.", ".").to_string(); + value = value.replace("flac_aac", "FLAC.AAC").to_string(); + value = value.replace("flac", "FLAC").to_string(); + value = value.replace("FLAC.H", "FLAC").to_string(); // - tokenizer.tokens.insert("audio_codec".to_string(), audio_codec); + value = value.replace(".&.", ".").to_string(); + + // + token.tokens.insert(KEY_AUDIO_CODEC.to_string(), value); } - if let Some(value) = captures.name("release_group") { + if let Some(value) = captures.name(KEY_RELEASE_GROUP) { let release_group = value.as_str().to_string(); - tokenizer.tokens.insert("release_group".to_string(), release_group); + token.tokens.insert(KEY_RELEASE_GROUP.to_string(), release_group); } - if let Some(value) = captures.name("country") { - tokenizer.tokens.insert("country".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_COUNTRY) { + token.tokens.insert(KEY_COUNTRY.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("subtitle_en") { - tokenizer.tokens.insert("subtitle".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SUBTITLE_EN) { + token.tokens.insert(KEY_SUBTITLE.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("subtitle_cn") { - tokenizer.tokens.insert("subtitle".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SUBTITLE_CN) { + token.tokens.insert(KEY_SUBTITLE.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("special") { - tokenizer.tokens.insert("special".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_SPECIAL) { + token.tokens.insert(KEY_SPECIAL.to_string(), value.as_str().to_string()); } - if let Some(value) = captures.name("format") { - tokenizer.tokens.insert("format".to_string(), value.as_str().to_string()); + if let Some(value) = captures.name(KEY_EXTENSION) { + token.tokens.insert(KEY_EXTENSION.to_string(), value.as_str().to_string()); } - // tracing::info!("tokenizer = {:?}",tokenizer); + // tracing::debug!("tokenizer = {:?}",tokenizer); - return Some(tokenizer); + return Some(token); } return None; } +pub(crate) fn input_before_replaces(input: &str) -> String { + let mut new_input = input.to_string(); + + if let Some(replaces) = &TV_RULES.before_replaces { + replaces.iter().for_each(|replace| { + if new_input.contains(&replace.src) { + tracing::debug!("replace.src = {:?} replace.target = {:?}", replace.src, replace.target); + new_input = new_input.replace(&replace.src, &replace.target); + } + }) + } + + if let Some(replaces) = &MOVIE_RULES.before_replaces { + replaces.iter().for_each(|replace| { + if new_input.contains(&replace.src) { + tracing::debug!("replace.src = {:?} replace.target = {:?}", replace.src, replace.target); + new_input = new_input.replace(&replace.src, &replace.target); + } + }) + } + + return new_input; +} + +pub fn input_before_process(before_token: &mut Token, input: &str) -> String { + let mut new_input = input.to_string(); + + // 使用正则提前识别发布方和视频格式 + let mut release_group_format: String = format!("{}.{}", wrap(KEY_RELEASE_GROUP), wrap(KEY_EXTENSION)); + release_group_format = release_group_format.replace(&wrap(KEY_RELEASE_GROUP), ®ex_item_release_group()); + release_group_format = release_group_format.replace(&wrap(KEY_EXTENSION), ®ex_item_extension()); + let release_group_format_regex = Regex::new(&release_group_format).unwrap(); + if let Some(capture) = release_group_format_regex.captures(&new_input) { + if let Some(value) = capture.name(KEY_RELEASE_GROUP) { + let mut value = value.as_str().to_string(); + tracing::debug!("release_group_format = {}", value); + new_input = new_input.replace(&format!(" {}.", value), ".").to_string(); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + new_input = new_input.replace(&format!("-{}.", value), ".").to_string(); + before_token.tokens.insert(KEY_RELEASE_GROUP.to_string(), value); + } + } + + // 使用正则移除公司,简化逻辑 + Regex::new(®ex_item_company()).unwrap().find_iter(&new_input.clone()).for_each(|m| { + let mut value = m.as_str().to_string(); + tracing::debug!("company = {}", value); + new_input = new_input.replace(&format!(" {} ", value), " ").to_string(); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + new_input = new_input.replace(&format!("{}.", value), "").to_string(); + new_input = new_input.replace(&format!(".{}", value), ".").to_string(); + new_input = new_input.replace(&format!("[{}]", value), "").to_string(); + }); + + // 使用正则移除国家,简化逻辑 + if let Some(capture) = Regex::new(®ex_item_country()).unwrap().captures(&new_input) { + if let Some(value) = capture.name(KEY_COUNTRY) { + let mut value = value.as_str().to_string(); + tracing::debug!("country = {}", value); + new_input = new_input.replace(&format!(" {} ", value), " ").to_string(); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + new_input = new_input.replace(&format!("{}.", value), "").to_string(); + new_input = new_input.replace(&format!(".{}", value), ".").to_string(); + } + } + + // 使用正则移除特典 + if let Some(capture) = Regex::new(®ex_item_special()).unwrap().captures(&new_input) { + if let Some(value) = capture.name(KEY_SPECIAL) { + let mut value = value.as_str().to_string(); + tracing::debug!("special = {}", value); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + new_input = new_input.replace(&format!("{}.", value), "").to_string(); + new_input = new_input.replace(&format!(".{}", value), ".").to_string(); + } + } + + // 使用正则移除字幕 + if let Some(capture) = Regex::new(®ex_item_subtitle_en()).unwrap().captures(&new_input) { + if let Some(value) = capture.name(KEY_SUBTITLE_EN) { + let mut value = value.as_str().to_string(); + tracing::debug!("subtitle_en = {}", value); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + new_input = new_input.replace(&format!("{}.", value), "").to_string(); + new_input = new_input.replace(&format!(".{}", value), ".").to_string(); + } + } + + // 使用正则移除字幕 + if let Some(capture) = Regex::new(®ex_item_subtitle_cn()).unwrap().captures(&new_input) { + if let Some(value) = capture.name(KEY_SUBTITLE_CN) { + let mut value = value.as_str().to_string(); + tracing::debug!("subtitle_cn = {}", value); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + new_input = new_input.replace(&format!("{}.", value), "").to_string(); + new_input = new_input.replace(&format!(".{}]", value), "]").to_string(); + new_input = new_input.replace(&format!("{}]", value), "]").to_string(); + } + } + + // 使用正则移除音频 + if let Some(capture) = Regex::new(®ex_item_audio_cn()).unwrap().captures(&new_input) { + if let Some(value) = capture.name(KEY_AUDIO_CN) { + let mut value = value.as_str().to_string(); + tracing::debug!("audio_cn = {}", value); + new_input = new_input.replace(&format!(" {}]", value), "]").to_string(); + } + } + + // 使用正则移除版本 + if let Some(capture) = Regex::new(®ex_item_version()).unwrap().captures(&new_input) { + if let Some(value) = capture.name(KEY_VERSION) { + let mut value = value.as_str().to_string(); + tracing::debug!("version = {}", value); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + } + } + + // 使用正则处理季集信息 + // $season$.$episode$ + if let Ok(regex) = Regex::new(®ex_item_season_and_episode()) { + if let Some(captures) = regex.captures(&new_input) { + let season = captures.name(KEY_SEASON).unwrap().as_str().to_string(); + let episode = captures.name(KEY_EPISODE).unwrap().as_str().to_string(); + let new_episode = episode.replace("EP", "E"); + let new_episode = new_episode.replace("Ep", "E"); + tracing::debug!("season = {}, episode = {}, new_episode = {}", season, episode, new_episode); + new_input = new_input + .replace(&format!("{}.{}", season, episode), &format!("{}{}", season, new_episode)) + .to_string(); + } + } + if let Ok(regex) = Regex::new(r"\.(?PS\d{2})(?PE[Pp]?\d{1})\.") { + if let Some(captures) = regex.captures(&new_input) { + let season = captures.name(KEY_SEASON).unwrap().as_str().to_string(); + let episode = captures.name(KEY_EPISODE).unwrap().as_str().to_string(); + let new_episode = episode.replace("EP", "E"); + let new_episode = new_episode.replace("Ep", "E"); + let new_episode: i32 = new_episode.replace("E", "").parse().unwrap(); + tracing::debug!( + "input = {}, season = {}, episode = {}, new_episode = {}", + new_input, + season, + episode, + new_episode + ); + new_input = new_input + .replace(&format!("{}{}", season, episode), &format!("{}E{:02}", season, new_episode)) + .to_string(); + } + } + if let Ok(regex) = Regex::new(r"\.(?PE[Pp]?\d{1})\.") { + if let Some(captures) = regex.captures(&new_input) { + let episode = captures.name(KEY_EPISODE).unwrap().as_str().to_string(); + let new_episode = episode.replace("EP", "E"); + let new_episode = new_episode.replace("Ep", "E"); + let new_episode: i32 = new_episode.replace("E", "").parse().unwrap(); + tracing::debug!("input = {}, episode = {}, new_episode = {}", new_input, episode, new_episode); + new_input = new_input.replace(&format!("{}", episode), &format!("E{:02}", new_episode)).to_string(); + } + } + + // 使用正则提前识别颜色 + Regex::new(®ex_item_color()).unwrap().find_iter(&new_input.clone()).for_each(|m| { + let value = m.as_str().to_string(); + tracing::debug!("color = {}", value); + // color的识别性不是很强,所以要做强制处理 + new_input = new_input.replace(&format!("{} ", value), "").to_string(); + new_input = new_input.replace(&format!("[{}_", value), "[").to_string(); + new_input = new_input.replace(&format!("_{}]", value), "]").to_string(); + new_input = new_input.replace(&format!(" {} ", value), " ").to_string(); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + new_input = new_input.replace(&format!(".{}-", value), "-").to_string(); + new_input = new_input.replace(&format!("_{}", value), "").to_string(); + }); + + // 使用正则提前识别发布方 + let mut release_group = format!("{}$", wrap(KEY_RELEASE_GROUP)).to_string(); + release_group = release_group.replace(&wrap(KEY_RELEASE_GROUP), ®ex_item_release_group()); + let release_group_regex = Regex::new(&release_group).unwrap(); + if let Some(capture) = release_group_regex.captures(&new_input) { + if let Some(value) = capture.name(KEY_RELEASE_GROUP) { + let mut value = value.as_str().to_string(); + new_input = new_input.replace(&format!(".{}", value), "").to_string(); + new_input = new_input.replace(&format!("-{}", value), "").to_string(); + tracing::debug!("release_group = {}", value); + before_token.tokens.insert(KEY_RELEASE_GROUP.to_string(), value); + } + } + + let mut release_group = format!("\\[{}\\]", wrap(KEY_RELEASE_GROUP)).to_string(); + release_group = release_group.replace(&wrap(KEY_RELEASE_GROUP), ®ex_item_release_group()); + let release_group_regex = Regex::new(&release_group).unwrap(); + if let Some(capture) = release_group_regex.captures(&new_input) { + if let Some(value) = capture.name(KEY_RELEASE_GROUP) { + let mut value = value.as_str().to_string(); + new_input = new_input.replace(&format!("[{}]", value), "").to_string().trim().to_string(); + tracing::debug!("release_group = {}", value); + before_token.tokens.insert(KEY_RELEASE_GROUP.to_string(), value); + } + } + + // 使用正则移除edition + Regex::new(®ex_item_edition()).unwrap().find_iter(&new_input.clone()).for_each(|m| { + let value = m.as_str().to_string(); + tracing::debug!("edition = {}", value); + new_input = new_input.replace(&format!("-{}", value), "").to_string(); + new_input = new_input.replace(&format!("{}-", value), "").to_string(); + new_input = new_input.replace(&format!(".{}.", value), ".").to_string(); + new_input = new_input.replace(&format!("{}.", value), "").to_string(); + }); + + new_input +} + fn input_before(rule: &Rule, input: &str) -> String { let mut new_input = input.to_string(); - if let Some(replaces) = &RULES.before_replaces { + // 处理正则替换逻辑 + if let Some(replaces) = &rule.regex_replaces { replaces.iter().for_each(|replace| { - new_input = new_input.replace(&replace.src, &replace.target); + let mut regex_str = replace.src.to_string(); + regex_str = regex_str.replace("$episode_cn$", ®ex_item_episode_cn()); + + tracing::debug!("regex_str = {}", regex_str); + + let regex = Regex::new(®ex_str).unwrap(); + if let Some(capture) = regex.captures(&new_input) { + if let Some(value) = capture.name("episode_cn") { + let target = replace.target.replace("$episode_cn$", value.as_str()); + let mut src = value.as_str().to_string(); + new_input = new_input.replace(&src, &target).to_string().trim().to_string(); + tracing::debug!("regex_replaces new_input = {}", new_input); + } + } }) } + // 处理普通的替换逻辑 if let Some(replaces) = &rule.replaces { replaces.iter().for_each(|replace| { - new_input = new_input.replace(&replace.src, &replace.target); + if new_input.contains(&replace.src) { + tracing::debug!("replaces src = {}, target = {}", replace.src, replace.target); + new_input = new_input.replace(&replace.src, &replace.target); + } }) } @@ -813,6 +1132,40 @@ fn read_rule_items(file_path: &str) -> Result> { Ok(data) } +fn regex_item_multi(name: &str, index: i32, regex: &Vec) -> String { + let mut combined = if index == 0 { + String::from(format!("(?P<{}>(", name)) + } else { + String::from(format!("(?P<{}{}>(", name, index)) + }; + let last_index = regex.len() - 1; + for (index, item) in regex.iter().enumerate() { + combined.push_str(item); + if index != (last_index) { + combined.push_str("|") + } + } + combined.push_str("))"); + return combined; +} + +fn regex_item_multi_str(name: &str, index: i32, regex: &Vec<&str>) -> String { + let mut combined = if index == 0 { + String::from(format!("(?P<{}>(", name)) + } else { + String::from(format!("(?P<{}{}>(", name, index)) + }; + let last_index = regex.len() - 1; + for (index, item) in regex.iter().enumerate() { + combined.push_str(item); + if index != (last_index) { + combined.push_str("|") + } + } + combined.push_str("))"); + return combined; +} + #[cfg(test)] mod tests { use regex::Regex; @@ -820,45 +1173,149 @@ mod tests { use crate::soda::meta::strong_match_token::*; #[test] - fn test_regex_item_episode_title_en() { - let regex = Regex::new(regex_item_episode_title_en()).unwrap(); - assert_eq!("1+1", regex.captures("1+1").unwrap().name("episode_title_en").unwrap().as_str()); - assert_eq!("2+2", regex.captures("2+2").unwrap().name("episode_title_en").unwrap().as_str()); - assert_eq!("Tape.Side.A", regex.captures("Tape.Side.A").unwrap().name("episode_title_en").unwrap().as_str()); - assert_eq!("Tape.1.Side.A", regex.captures("Tape.1.Side.A").unwrap().name("episode_title_en").unwrap().as_str()); - assert_eq!("Tape.Side.A.1", regex.captures("Tape.Side.A.1").unwrap().name("episode_title_en").unwrap().as_str()); + fn test_regex_item_title_number_en() { + let regex = Regex::new(®ex_item_title_number_en()).unwrap(); + assert_eq!( + "Attenborough 60 Years in the Wild", + regex + .captures("Attenborough 60 Years in the Wild") + .unwrap() + .name("title_number_en") + .unwrap() + .as_str() + ); + assert_eq!( + "Reply.1988", + regex.captures("Reply.1988").unwrap().name("title_number_en").unwrap().as_str() + ); + assert_eq!( + "John Wick Chapter 4", + regex.captures("John Wick Chapter 4").unwrap().name("title_number_en").unwrap().as_str() + ); + assert_eq!( + "13.Reasons.Why", + regex.captures("13.Reasons.Why").unwrap().name("title_number_en").unwrap().as_str() + ); + assert_eq!( + "Evangelion 2.22 You Can (Not) Advance", + regex + .captures("Evangelion 2.22 You Can (Not) Advance") + .unwrap() + .name("title_number_en") + .unwrap() + .as_str() + ); + assert_eq!( + "Jujutsu Kaisen 0", + regex.captures("Jujutsu Kaisen 0").unwrap().name("title_number_en").unwrap().as_str() + ); + } + + #[test] + fn test_regex_item_episode_number2() { + let re = Regex::new(r"(?P[!×I&'-. A-Za-z]+)\.(?P\d{2,3})\.(?P240[Ppi]|360[Ppi]|480[Ppi]|576[Pp]|720[Ppi]|1080[Ppi]|1440[Ppi]|2160[Ppi]|4320[Ppi]|4[Kk])\.(?P([XxHh]\.?26[45]|(HEVC|Hevc)|MPEG-\d|VC-\d|MPEG2|VP9|AV1|VC1|AVC))\.(?P(mp4|mkv|iso|rmvb|avi|mov|mepg|mpg|wmv|3gp|asf|m4v|flv))$").unwrap(); + let text = "Dragon.Ball.150.480p.x264.mkv"; + + if let Some(caps) = re.captures(text) { + println!("Title (en): {}", &caps["title_en"]); + println!("Episode Number: {}", &caps["episode_number"]); + println!("Resolution: {}", &caps["resolution"]); + println!("Video Codec: {}", &caps["video_codec"]); + println!("Format: {}", &caps["extension"]); + } else { + println!("No match found"); + } + } + + #[test] + fn test_regex_item_episode() { + let regex = Regex::new(®ex_item_episode()).unwrap(); + assert_eq!("E01", regex.captures("E01").unwrap().name("episode").unwrap().as_str()); + assert_eq!("E001", regex.captures("E001").unwrap().name("episode").unwrap().as_str()); + } + + #[test] + fn test_regex_item_episode_number() { + let regex = Regex::new(®ex_item_episode_number()).unwrap(); + assert_eq!("150", regex.captures("150").unwrap().name("episode_number").unwrap().as_str()); } #[test] - fn test_regex() { - Regex::new(regex_item_country()).unwrap(); - Regex::new(regex_item_title_cn()).unwrap(); - Regex::new(regex_item_title_en()).unwrap(); - Regex::new(regex_item_title_number_en()).unwrap(); - Regex::new(regex_item_year()).unwrap(); - Regex::new(regex_item_season_episode()).unwrap(); - Regex::new(regex_item_season_episode_split()).unwrap(); - Regex::new(regex_item_season_episode_episode()).unwrap(); - Regex::new(regex_item_season()).unwrap(); - Regex::new(regex_item_episode()).unwrap(); - Regex::new(regex_item_episode_title_en()).unwrap(); - Regex::new(regex_item_resolution()).unwrap(); - Regex::new(regex_item_version()).unwrap(); - Regex::new(®ex_item_source()).unwrap(); - Regex::new(®ex_item_company()).unwrap(); - Regex::new(®ex_item_video_codec()).unwrap(); - Regex::new(®ex_item_color()).unwrap(); - Regex::new(®ex_item_audio_codec()).unwrap(); - Regex::new(®ex_item_release_group()).unwrap(); - Regex::new(®ex_item_format()).unwrap(); - Regex::new(®ex_item_mix_numbers_letters()).unwrap(); + fn test_regex_item_episode_title_jp() { + let regex = Regex::new(®ex_item_episode_title_jp()).unwrap(); + assert_eq!( + "桜舞うソラに", + regex.captures("桜舞うソラに").unwrap().name("episode_title_jp").unwrap().as_str() + ); + } + + #[test] + fn test_regex_item_title_number_cn() { + let regex = Regex::new(®ex_item_title_number_cn()).unwrap(); + assert_eq!( + "拜托了!8小时", + regex.captures("拜托了!8小时").unwrap().name("title_number_cn").unwrap().as_str() + ); + assert_eq!( + "风味人间3·大海小鲜", + regex.captures("风味人间3·大海小鲜").unwrap().name("title_number_cn").unwrap().as_str() + ); + assert_eq!( + "风味人间4", + regex.captures("风味人间4").unwrap().name("title_number_cn").unwrap().as_str() + ); + } + + #[test] + fn test_regex_item_title_cn() { + let regex = Regex::new(®ex_item_title_cn()).unwrap(); + assert_eq!( + "与摩根·弗里曼一起穿越虫洞", + regex.captures("与摩根·弗里曼一起穿越虫洞").unwrap().name("title_cn").unwrap().as_str() + ); + } + + #[test] + fn test_regex_item_episode_title_en() { + let regex = Regex::new(®ex_item_episode_title_en()).unwrap(); + assert_eq!("1+1", regex.captures("1+1").unwrap().name("episode_title_en").unwrap().as_str()); + assert_eq!("2+2", regex.captures("2+2").unwrap().name("episode_title_en").unwrap().as_str()); + assert_eq!( + "Tape.Side.A", + regex.captures("Tape.Side.A").unwrap().name("episode_title_en").unwrap().as_str() + ); + assert_eq!( + "Tape.1.Side.A", + regex.captures("Tape.1.Side.A").unwrap().name("episode_title_en").unwrap().as_str() + ); + assert_eq!( + "Tape.Side.A.1", + regex.captures("Tape.Side.A.1").unwrap().name("episode_title_en").unwrap().as_str() + ); + assert_eq!( + "994.Cars.Long", + regex.captures("994.Cars.Long").unwrap().name("episode_title_en").unwrap().as_str() + ); + assert_eq!( + "Route.666", + regex.captures("Route.666").unwrap().name("episode_title_en").unwrap().as_str() + ); + assert_eq!( + "Docket.No.11-19-41-73", + regex + .captures("Docket.No.11-19-41-73") + .unwrap() + .name("episode_title_en") + .unwrap() + .as_str() + ); } #[test] fn test_parse4() { // $title_cn$.$title_en$.$year$.$season$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$ let input = "凡人修仙传.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H.264.AAC-OurTV"; - let regex = Regex::new(r"\((?P\d{4}\.\d{2}\.\d{2})\)(?P(([\\(\\)&'-. 0-9A-Za-z])+))-\[(?P(240[Ppi]|360[Ppi]|720[Ppi]|1080[Ppi]|1440[Ppi]|2160[Ppi]|4320[Ppi]|4[Kk]))\]\[(?P(Ger|GER|USA)).(?P(AVC.REMUX|Blu-ray Remux|Blu-ray.REMUX|Blu-ray.Remux|BluRay.REMUX|BluRay.Remux|Blu-Ray|BluRay|Bluray|BDRIP|BDRip|BDrip|WEBRip|WEBrip|WEB-DL|WEB|Blu-?[Rr]ay|DVDRip|HDTVRip|BRRip|HDRip|CAM|HDTV|BD[.-]?Remux|REMUX|Remux))\].(?P(mp4|mkv|ts|iso|rmvb|avi|mov|mepg|mpg|wmv|3gp|asf|m4v|flv|m2ts))").unwrap(); + let regex = Regex::new(r"\((?P\d{4}\.\d{2}\.\d{2})\)(?P(([\\(\\)&'-. 0-9A-Za-z])+))-\[(?P(240[Ppi]|360[Ppi]|720[Ppi]|1080[Ppi]|1440[Ppi]|2160[Ppi]|4320[Ppi]|4[Kk]))\]\[(?P(Ger|GER|USA)).(?P(AVC.REMUX|Blu-ray Remux|Blu-ray.REMUX|Blu-ray.Remux|BluRay.REMUX|BluRay.Remux|Blu-Ray|BluRay|Bluray|BDRIP|BDRip|BDrip|WEBRip|WEBrip|WEB-DL|WEB|Blu-?[Rr]ay|DVDRip|HDTVRip|BRRip|HDRip|CAM|HDTV|BD[.-]?Remux|REMUX|Remux))\].(?P(mp4|mkv|ts|iso|rmvb|avi|mov|mepg|mpg|wmv|3gp|asf|m4v|flv|m2ts))").unwrap(); if regex.is_match(input) { println!("match"); } diff --git a/soda_resource_tools_lib/src/soda/meta/token.rs b/soda_resource_tools_lib/src/soda/meta/token.rs index 750a23e8..c60ec891 100644 --- a/soda_resource_tools_lib/src/soda/meta/token.rs +++ b/soda_resource_tools_lib/src/soda/meta/token.rs @@ -1,40 +1,26 @@ use std::collections::HashMap; -use crate::soda::global::REGEX_MT_EXT; +use crate::soda::{ + entity::{MetaContext, Token}, + global::REGEX_MT_EXT, +}; -use super::strong_match_token::{build_mt_dir_tokens, build_mt_file_tokens}; +use super::strong_match_token; -pub(crate) enum Token { - FastToken(String), - StrongMatchToken(String), -} +pub(crate) fn tokens(meta_context: &mut MetaContext) -> Option { + let mut before_token = Token::new(); + meta_context.input = meta_context.cur_file_name.to_string(); -impl Token { - pub(crate) fn tokens(&self) -> Option { - match self { - Token::FastToken(title) => None, - Token::StrongMatchToken(title) => { - if REGEX_MT_EXT.is_match(title) { - build_mt_file_tokens(title) - } else { - build_mt_dir_tokens(title) - } - } - } - } -} + tracing::debug!("meta_context.input 1 = {:?}", meta_context.input); -pub(crate) fn build_tokens(title: &str) -> Option { - if let Some(ret) = Token::FastToken(title.to_string()).tokens() { - return Some(ret); - } - if let Some(ret) = Token::StrongMatchToken(title.to_string()).tokens() { - return Some(ret); - } - return None; -} + meta_context.input = strong_match_token::input_before_replaces(&meta_context.input); + meta_context.input = strong_match_token::input_before_process(&mut before_token, &meta_context.input); -#[derive(Debug, Clone)] -pub(crate) struct Tokens { - pub(crate) tokens: HashMap, + tracing::debug!("meta_context.input 2 = {:?}", meta_context.input); + + if REGEX_MT_EXT.is_match(&meta_context.input) { + strong_match_token::build_mt_file_tokens(meta_context, &mut before_token) + } else { + strong_match_token::build_mt_dir_tokens(&meta_context.input, &mut before_token) + } } diff --git a/soda_resource_tools_lib/src/soda/request.rs b/soda_resource_tools_lib/src/soda/request.rs index 825f9cbe..5e6beb8e 100644 --- a/soda_resource_tools_lib/src/soda/request.rs +++ b/soda_resource_tools_lib/src/soda/request.rs @@ -20,9 +20,9 @@ use super::{ /// 如果请求网络失败,则返回错误。 /// 如果请求网络成功,则解析成JSON缓存后返回。 pub(crate) fn blocking_request_value_with_cache(cache_type: CacheType, method: &str, url: &str) -> Result { + tracing::debug!("request url = {:?}", url); let cache = cache::cache_get(&cache_type, url); if cache.is_some() { - tracing::info!("request cache hit url = {}", url); let cache_text = cache.unwrap(); match serde_json::from_str(&cache_text) { Ok(value) => { @@ -58,13 +58,16 @@ pub(crate) fn blocking_request_value_with_cache(cache_type: CacheType, method: & } pub(crate) fn blocking_request(method: &str, url: &str) -> Result { - Ok(if method == "GET" { blocking_request_get(url)? } else { blocking_request_post(url)? }) + Ok(if method == "GET" { + blocking_request_get(url)? + } else { + blocking_request_post(url)? + }) } pub(crate) fn blocking_request_post(url: &str) -> Result { let response = reqwest::blocking::Client::new().post(url).send().on_err_inspect(|e| { tracing::error!("request error {:?}", e); - utils::write_request_error(format!("url = {} e = {:?}", url, e)); })?; Ok(response) } @@ -72,7 +75,6 @@ pub(crate) fn blocking_request_post(url: &str) -> Result Result { let response = reqwest::blocking::get(url).on_err_inspect(|e| { tracing::error!("request error {:?}", e); - utils::write_request_error(format!("url = {} e = {:?}", url, e)); })?; Ok(response) } @@ -90,7 +92,6 @@ pub(crate) fn blocking_get_request_and_download_file(url: &str, image_path: &std fn blocking_request_bytes_with_cache(cache_type: CacheType, method: &str, url: &str) -> Result { let cache = cache::cache_get_bytes(&cache_type, url); if cache.is_some() { - tracing::info!("request cache hit url = {}", url); return Ok(cache.unwrap()); } diff --git a/soda_resource_tools_lib/src/soda/scraper.rs b/soda_resource_tools_lib/src/soda/scraper.rs index b4b39874..a6f48120 100644 --- a/soda_resource_tools_lib/src/soda/scraper.rs +++ b/soda_resource_tools_lib/src/soda/scraper.rs @@ -4,7 +4,7 @@ use super::entity::{MTInfo, MTMetadata, ScrapeConfig}; /// 刮削要整理的资源 pub(crate) fn scrape_metadata(scrape_config: &ScrapeConfig, mt_meta: &MTMetadata, mt_info: &MTInfo, path: &str) { - tracing::info!("scrape_metadata mt_info = {:?} path = {:?} ", mt_info.title(), path); + tracing::debug!("scrape_metadata mt_info = {:?} path = {:?} ", mt_info.title(), path); scraper::gen_scrape_files(scrape_config, mt_meta, mt_info, path); - tracing::info!("scrape_metadata success"); + tracing::debug!("scrape_metadata success"); } diff --git a/soda_resource_tools_lib/src/soda/tmdb.rs b/soda_resource_tools_lib/src/soda/tmdb.rs index 33fb4e52..2c0aee1f 100644 --- a/soda_resource_tools_lib/src/soda/tmdb.rs +++ b/soda_resource_tools_lib/src/soda/tmdb.rs @@ -1,17 +1,24 @@ +use std::collections::HashMap; +use std::f32::consts::E; +use std::fs; + use once_cell::sync::Lazy; use regex::Regex; use serde::de::value; +use serde::Deserialize; use serde_json::Value; -use tracing::info; +use tracing::debug; use crate::soda::entity::{MTMetadata, MTType}; use crate::soda::extension_option::OptionExtensions; -use self::entity::{TmdbEpisode, TmdbSeason, TmdbTV}; +use self::entity::{TmdbEpisode, TmdbMovie, TmdbSeason, TmdbTV}; use super::entity::{MTInfo, SodaError}; +use super::LIB_CONFIG; pub(crate) mod entity; +mod movie; mod request; pub(crate) mod scraper; mod search; @@ -27,149 +34,455 @@ fn get_info(tmdb_id: &str) {} fn get_movie_detail(tmdb_id: &str) {} -pub fn search_tv_by_season(title_cn: &str, title_en: &str, season_year: &str) -> Result { - tracing::info!("search tv title_cn = {:?} title_en = {:?} season_year = {}", title_cn, title_en, season_year); +pub fn search_tv_by_name(title_cn: &str, title_en: &str) -> Result { + tracing::debug!("search_tv_by_name title_cn = {:?} title_en = {:?} ", title_cn, title_en); + + let mut season_title = ""; + + // 先使用英文标题搜索 + if !title_en.is_empty() { + season_title = title_en; + if let Ok(tv) = find_tv_by_language(season_title, "", title_cn, title_en, "en-US") { + return Ok(tv); + } + if let Ok(tv) = find_tv_by_language(season_title, "", title_cn, title_en, "zh-CN") { + return Ok(tv); + } + } + + // 再使用中文标题搜索 + if !title_cn.is_empty() { + season_title = title_cn; + // 非第一季不使用年份搜索 + if let Ok(tv) = find_tv_by_language(season_title, "", title_cn, title_en, "en-US") { + return Ok(tv); + } + if let Ok(tv) = find_tv_by_language(season_title, "", title_cn, title_en, "zh-CN") { + return Ok(tv); + } + } + return Err(SodaError::Str("search_tv_by_name not found")); +} + +pub fn search_tv_by_season(title_cn: &str, title_en: &str, season_number: i64, season_year: &str) -> Result { + tracing::debug!( + "search_tv_by_season title_cn = {:?} title_en = {:?} season_number = {:?} season_year = {:?}", + title_cn, + title_en, + season_number, + season_year + ); + + let mut season_title = ""; + + // 先使用英文标题搜索 + if !title_en.is_empty() { + season_title = title_en; + if season_number == 1 { + // 第一季使用年份准确搜索 + if let Ok(tv) = find_tv_by_language(season_title, season_year, title_cn, title_en, "en-US") { + return Ok(tv); + } + if let Ok(tv) = find_tv_by_language(season_title, season_year, title_cn, title_en, "zh-CN") { + return Ok(tv); + } + } else { + // 非第一季不使用年份搜索 + if let Ok(tv) = find_tv_by_language(season_title, "", title_cn, title_en, "en-US") { + return Ok(tv); + } + if let Ok(tv) = find_tv_by_language(season_title, "", title_cn, title_en, "zh-CN") { + return Ok(tv); + } + } + } + + // 再使用中文标题搜索 + if !title_cn.is_empty() { + season_title = title_cn; + if season_number == 1 { + // 第一季使用年份准确搜索 + if let Ok(tv) = find_tv_by_language(season_title, season_year, title_cn, title_en, "en-US") { + return Ok(tv); + } + if let Ok(tv) = find_tv_by_language(season_title, season_year, title_cn, title_en, "zh-CN") { + return Ok(tv); + } + } else { + // 非第一季不使用年份搜索 + if let Ok(tv) = find_tv_by_language(season_title, "", title_cn, title_en, "en-US") { + return Ok(tv); + } + if let Ok(tv) = find_tv_by_language(season_title, "", title_cn, title_en, "zh-CN") { + return Ok(tv); + } + } + } + + return Err(SodaError::Str("search_tv_by_season not found")); +} + +fn find_tv_by_language(title: &str, season_year: &str, title_cn: &str, title_en: &str, language: &str) -> Result { + // 先使用年份搜索 + let mut tvs = search::search_tv_with_language(title, None, Some(season_year), None, 1, language)?; + + // 如果没有搜索到结果则移除年份再次搜索 + if tvs.len() == 0 { + tvs = search::search_tv_with_language(title, None, None, None, 1, language)?; + return find_tv_by_name_and_year(&tvs, title_cn, title_en, ""); + } + // 如果搜索结果只有一个,那么直接使用这个作为查询结果 + else if tvs.len() == 1 { + return Ok(tvs.get(0).ok_or("get tv error")?.clone()); + } + // 如果搜索到结果则使用年份搜索 + else { + return find_tv_by_name_and_year(&tvs, title_cn, title_en, season_year); + } +} + +pub fn search_movie(title_cn: &str, title_en: &str, year: Option<&str>) -> Result { + let mut title = ""; + + // 先使用英文标题搜索 + if !title_en.is_empty() { + title = title_en; + if let Ok(tv) = find_movie_by_language(title, year, title_cn, title_en, "en-US") { + return Ok(tv); + } + if let Ok(tv) = find_movie_by_language(title, year, title_cn, title_en, "zh-CN") { + return Ok(tv); + } + } + + // 再使用中文标题搜索 + if !title_cn.is_empty() { + title = title_cn; + if let Ok(tv) = find_movie_by_language(title, year, title_cn, title_en, "en-US") { + return Ok(tv); + } + if let Ok(tv) = find_movie_by_language(title, year, title_cn, title_en, "zh-CN") { + return Ok(tv); + } + } - // search tv 优先使用英文标题 - let title = if title_en.is_empty() { title_cn } else { title_en }; + return Err(SodaError::Str("search_movie not found")); +} - let tvs = search::search_tv(title, Some(season_year), None, None, 1)?; +fn find_movie_by_language(title: &str, year: Option<&str>, title_cn: &str, title_en: &str, language: &str) -> Result { + // 先使用年份搜索 + let mut movies = search::search_movie_with_language(title, year, language)?; - return find_tv_by_name(&tvs, title); + // 如果没有搜索到结果则移除年份再次搜索 + if movies.len() == 0 { + movies = search::search_tv_with_language(title, None, None, None, 1, language)?; + return find_movie_by_name_and_year(&movies, title_cn, title_en, None); + } + // 如果搜索结果只有一个,那么直接使用这个作为查询结果 + else if movies.len() == 1 { + return Ok(movies.get(0).ok_or("get movies error")?.clone()); + } + // 如果搜索到结果则使用年份搜索 + else { + return find_movie_by_name_and_year(&movies, title_cn, title_en, year); + } +} + +/// 根据名称搜索电影获取基础信息 +/// +// { +// "page": 1, +// "results": [ +// { +// "adult": false, +// "backdrop_path": "/4HodYYKEIsGOdinkGi2Ucz6X9i0.jpg", +// "genre_ids": [ +// 16, +// 28, +// 12, +// 878 +// ], +// "id": 569094, +// "original_language": "en", +// "original_title": "Spider-Man: Across the Spider-Verse", +// "overview": "讲述了新生代蜘蛛侠迈尔斯(沙梅克·摩尔 Shameik Moore 配音)携手蜘蛛格温(海莉·斯坦菲尔德 Hailee Steinfeld 配音),穿越多元宇宙踏上更宏大的冒险征程的故事。面临每个蜘蛛侠都会失去至亲的宿命,迈尔斯誓言打破命运魔咒,找到属于自己的英雄之路。而这个决定和蜘蛛侠2099(奥斯卡·伊萨克 Oscar Is aac 配音)所领军的蜘蛛联盟产生了极大冲突,一场以一敌百的蜘蛛侠大内战即将拉响!", +// "popularity": 357.459, +// "poster_path": "/jBmJZDiJ2h6DimGROpxWlPh1xIo.jpg", +// "release_date": "2023-05-31", +// "title": "蜘蛛侠:纵横宇宙", +// "video": false, +// "vote_average": 8.376, +// "vote_count": 5548 +// } +// ], +// "total_pages": 1, +// "total_results": 1 +// } +// curl --request GET \ +// --url 'https://api.themoviedb.org/3/search/movie?query=Spider%20Man%20Across%20the%20Spider%20Verse&include_adult=false&language=zh-CN&page=1&year=2023' \ +// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2ZjViOTZkMGQ3MjUzMTE3YzQ0OTYzYTBjZThhYTZmMiIsInN1YiI6IjVjYmQzOTlmYzNhMzY4MTM2OTg1ZmM4ZSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.0ogVgNoOHpOswqSpl5Zg-_zxWEvAL2CO1TwIerbRbeo' \ +// --header 'accept: application/json' +fn find_movie_by_name_and_year(movies: &Vec, title_cn: &str, title_en: &str, year: Option<&str>) -> Result { + tracing::debug!( + "find_movie_by_name_and_year title_cn = {:?} title_en = {:?} year = {:?}", + title_cn, + title_en, + year + ); + + static MOVIE_NAME_REPLACE_REGEX: Lazy = Lazy::new(|| Regex::new(r"[ ,\.\-'\?:\|]").unwrap()); + + let title_cn = title_cn.to_lowercase(); + let title_cn = MOVIE_NAME_REPLACE_REGEX.replace_all(&title_cn, ""); + + let title_en = title_en.to_lowercase(); + let title_en = MOVIE_NAME_REPLACE_REGEX.replace_all(&title_en, ""); + + // 如果有年份则根据年份搜索 + if let Some(year) = year { + for movie in movies { + tracing::debug!("find_movie_by_name_and_year movie = {:?}", movie); + + let id = movie.get("id").ok_or("get id error")?.as_i64().unwrap(); + + let movie_name = movie.get("title").ok_or("get name error")?.as_str().unwrap().to_lowercase(); + let movie_name = MOVIE_NAME_REPLACE_REGEX.replace_all(&movie_name, ""); + let movie_original_title = movie + .get("original_title") + .ok_or("get original_title error")? + .as_str() + .unwrap() + .to_lowercase(); + let movie_original_title = MOVIE_NAME_REPLACE_REGEX.replace_all(&movie_original_title, ""); + + let is_name = movie_name == title_cn || movie_original_title == title_cn || movie_name == title_en || movie_original_title == title_en; + + let release_year = movie.get("release_date").ok_or("get release_date error")?.as_str().unwrap().to_string(); + if release_year.len() > 4 { + let release_year = (release_year[0..4]).to_string(); + if release_year == year && is_name { + return Result::Ok(movie.clone()); + } + } + } + } + + // 根据名称搜索 + for movie in movies { + let id = movie.get("id").ok_or("get id error")?.as_i64().unwrap(); + + tracing::debug!("find_movie_by_name_and_year movie = {:?}", movie); + + let movie_name: String = movie.get("title").ok_or("get name error")?.as_str().unwrap().to_lowercase(); + let movie_name = MOVIE_NAME_REPLACE_REGEX.replace_all(&movie_name, ""); + let movie_original_title = movie + .get("original_title") + .ok_or("get original_title error")? + .as_str() + .unwrap() + .to_lowercase(); + let movie_original_title = MOVIE_NAME_REPLACE_REGEX.replace_all(&movie_original_title, ""); + + tracing::debug!( + "find_movie_by_name_and_year title_cn = {:?} title_en = {:?} movie_name = {:?} movie_original_title = {:?}", + title_cn, + title_en, + movie_name, + movie_original_title + ); + + let is_name = movie_name == title_cn || movie_original_title == title_cn || movie_name == title_en || movie_original_title == title_en; + if is_name { + return Result::Ok(movie.clone()); + } + } + + return Err(SodaError::String(format!( + "find movie on movies failed tvs = {}", + serde_json::to_string(&movies).unwrap() + ))); } /// 根据名称搜索电视剧获取基础信息 -fn find_tv_by_name(tvs: &Vec, name: &str) -> Result { - for tv in tvs { - static TV_NAME_REPLACE_REGEX: Lazy = Lazy::new(|| Regex::new(r"[ \.-]").unwrap()); +/// +fn find_tv_by_name_and_year(tvs: &Vec, title_cn: &str, title_en: &str, first_release_year: &str) -> Result { + static TV_NAME_REPLACE_REGEX: Lazy = Lazy::new(|| Regex::new(r"[ ,\.\-'\?:\|]").unwrap()); + let title_cn = title_cn.to_lowercase(); + let title_cn = TV_NAME_REPLACE_REGEX.replace_all(&title_cn, ""); + + let title_en = title_en.to_lowercase(); + let title_en = TV_NAME_REPLACE_REGEX.replace_all(&title_en, ""); + + // 如果有年份则根据年份搜索 + if !first_release_year.is_empty() { + for tv in tvs { + let id = tv.get("id").ok_or("get id error")?.as_i64().unwrap(); + + let tv_name = tv.get("name").ok_or("get name error")?.as_str().unwrap().to_lowercase(); + let tv_name = TV_NAME_REPLACE_REGEX.replace_all(&tv_name, ""); + let tv_original_name = tv.get("original_name").ok_or("get original_name error")?.as_str().unwrap().to_lowercase(); + let tv_original_name = TV_NAME_REPLACE_REGEX.replace_all(&tv_original_name, ""); + + let is_name = tv_name == title_cn || tv_original_name == title_cn || tv_name == title_en || tv_original_name == title_en; + + let release_year = tv.get("first_air_date").ok_or("get first_air_date error")?.as_str().unwrap().to_string(); + if release_year.len() > 4 { + let release_year = (release_year[0..4]).to_string(); + if release_year == first_release_year && is_name { + return Result::Ok(tv.clone()); + } + } + } + } + + // 根据名称搜索 + for tv in tvs { let id = tv.get("id").ok_or("get id error")?.as_i64().unwrap(); - let name = name.to_lowercase(); - let name = TV_NAME_REPLACE_REGEX.replace_all(&name, ""); + let tv_name = tv.get("name").ok_or("get name error")?.as_str().unwrap().to_lowercase(); let tv_name = TV_NAME_REPLACE_REGEX.replace_all(&tv_name, ""); let tv_original_name = tv.get("original_name").ok_or("get original_name error")?.as_str().unwrap().to_lowercase(); let tv_original_name = TV_NAME_REPLACE_REGEX.replace_all(&tv_original_name, ""); - if tv_name == name || tv_original_name == name { + + let is_name = tv_name == title_cn || tv_original_name == title_cn || tv_name == title_en || tv_original_name == title_en; + if is_name { return Result::Ok(tv.clone()); } } - return Err(SodaError::String(format!("find tv on tvs failed name = {} tvs = {:?}", name, tvs))); -} - -pub fn search_tv_by_name(name: &str) -> Result { - tracing::info!("search tv name = {:?}", name); - let tvs = search::search_tv(name, None, None, None, 1)?; - - return find_tv_by_name(&tvs, name); + return Err(SodaError::String(format!( + "find tv on tvs failed tvs = {}", + serde_json::to_string(&tvs).unwrap() + ))); } -pub(crate) fn search_movie(name: &str, year: &str, season: &str) -> Option { - if name.is_empty() { - return None; +/// 识别影视资源 +pub(crate) fn recognize_mt(mt_infos: &mut HashMap, mt_meta: &mut MTMetadata) -> Result { + if mt_meta.is_tv() { + return recognize_mt_tv(mt_infos, mt_meta); + } else if mt_meta.is_movie() { + return recognize_mt_movie(mt_infos, mt_meta); } - return None; + return Err(SodaError::Str("not support recognize mt")); } -/// 识别影视资源 -pub(crate) fn recognize_mt(meta: &mut MTMetadata) -> Option { - let mut mt_info: Option = None; +fn recognize_mt_movie(mt_infos: &mut HashMap, mt_meta: &mut MTMetadata) -> Result { + // 缓存信息 + let key: String = format!("{}-{}", &mt_meta.title_en, &mt_meta.title_cn); - if meta.is_tv() { + let mt_info = if let Some(cache_mt_info) = mt_infos.get_mut(&key) { + tracing::debug!("recognize_mt_movie cache mt_info = {:?}", cache_mt_info); + cache_mt_info + } else { // 根据名称搜索电视剧获取基础信息 - let mut tv_value = match &meta.year { - None => search_tv_by_name(meta.title()), - Some(year) => search_tv_by_season(&meta.title_cn, &meta.title_en, year), - }; - - // 根据TMDBID获取详细信息 - match tv_value { - Ok(value) => { - if let Some(value) = value.get("id") { - let tmdb_id = value.as_i64().unwrap().to_string(); - // 获取电视剧详细信息 - match tmdb_tv_details(&tmdb_id) { - Ok(tv_detail) => mt_info = Some(MTInfo::new(tv_detail)), - Err(e) => { - tracing::error!("search tv detail failed, name = {:?} error = {:?}", meta.title(), e); - } - } + let mut movie_value = if !mt_meta.year.is_empty() { + // 按按照AKA查询 as known as + if !mt_meta.aka_title_en_first.is_empty() && !mt_meta.aka_title_en_second.is_empty() { + if let Ok(ret) = search_movie(&mt_meta.title_cn, &mt_meta.aka_title_en_first, Some(&mt_meta.year)) { + Ok(ret) } else { - tracing::error!("get tv id failed, name = {:?}", meta.title()); + search_movie(&mt_meta.title_cn, &mt_meta.aka_title_en_second, Some(&mt_meta.year)) } } - Err(e) => { - tracing::error!("search tv failed, name = {:?} error = {:?}", meta.title(), e); + // 再按照普通名字查询 + else { + search_movie(&mt_meta.title_cn, &mt_meta.title_en, Some(&mt_meta.year)) } - } - } else if meta.is_movie() { - // search_movie(meta.title(), &meta.year.clone().unwrap(), &meta.season); - } + } else { + search_movie(&mt_meta.title_cn, &mt_meta.title_en, None) + }?; - mt_info.is_some_mut_then(|info| { - meta.merge(info); + // 根据TMDBID获取电视剧详细信息 + let id_value = movie_value + .get("id") + .ok_or(SodaError::String(format!("get movie id failed, name = {:?}", mt_meta.title())))?; + let tmdb_id = id_value.as_i64().unwrap().to_string(); + let mut mt_info = MTInfo::new_movie(tmdb_movie_details(&tmdb_id)?); + mt_infos.insert(key.clone(), mt_info); - if let Some(season_number) = meta.season_number() { - // 获取电视剧季的详细信息 - match tmdb_tv_season_detail(info.tmdb_id(), season_number) { - Ok(season_detail) => info.insert_tv_season(season_number, season_detail), - Err(e) => tracing::error!("search tv season detail failed, name = {:?} season = {:?} error = {:?}", meta.title(), season_number, e), - } + mt_infos.get_mut(&key).unwrap() + }; - // 获取电视剧集的详细信息 - if let Some(episode_number) = meta.episode_number() { - match tmdb_tv_season_episode_detail(info.tmdb_id(), season_number, episode_number) { - Ok(episode_detail) => info.insert_tv_season_episode(season_number, episode_number, episode_detail), - Err(e) => tracing::error!("search tv season episode detail failed, name = {:?} season = {:?} episode = {:?} error = {:?}", meta.title(), season_number, episode_number, e), - } - } else { - tracing::error!("episode is empty"); - } + // 合并元数据 + mt_meta.merge_movie(mt_info); + + return Ok(key); +} + +/// 识别影视资源 +pub(crate) fn recognize_mt_tv(mt_infos: &mut HashMap, mt_meta: &mut MTMetadata) -> Result { + // 缓存信息 + let key: String = format!("{}-{}", &mt_meta.title_en, &mt_meta.title_cn); + + let mt_info = if let Some(cache_mt_info) = mt_infos.get_mut(&key) { + tracing::debug!("recognize_mt_tv cache mt_info = {:?}", cache_mt_info); + cache_mt_info + } else { + // 根据名称搜索电视剧获取基础信息 + let mut tv_value = if !mt_meta.year.is_empty() && !mt_meta.season.is_empty() { + let season_number = mt_meta.season_number().unwrap(); + search_tv_by_season(&mt_meta.title_cn, &mt_meta.title_en, season_number, &mt_meta.year) } else { - tracing::error!("season is empty"); - } - }); + search_tv_by_name(&mt_meta.title_cn, &mt_meta.title_en) + }?; + + // 根据TMDBID获取电视剧详细信息 + let id_value = tv_value + .get("id") + .ok_or(SodaError::String(format!("get tv id failed, name = {:?}", mt_meta.title())))?; + let tmdb_id = id_value.as_i64().unwrap().to_string(); + let mut mt_info = MTInfo::new_tv(tmdb_tv_details(&tmdb_id)?); + mt_infos.insert(key.clone(), mt_info); + + mt_infos.get_mut(&key).unwrap() + }; + + // 合并元数据 + mt_meta.merge_tv(mt_info); - return mt_info; + // 获取电视剧季的详细信息 + let season_number = mt_meta.season_number().ok_or(SodaError::Str("season is empty"))?; + let season_detail = tmdb_tv_season_detail(mt_info.tmdb_id(), season_number)?; + mt_meta.merge_season(&season_detail); + mt_info.insert_tv_season(season_number, season_detail); + + // 获取电视剧集的详细信息 + let episode_number = mt_meta.episode_number().ok_or(SodaError::Str("episode is empty"))?; + let episode_detail = tmdb_tv_season_episode_detail(mt_info.tmdb_id(), season_number, episode_number)?; + mt_info.insert_tv_season_episode(season_number, episode_number, episode_detail); + + return Ok(key); } /// 查询电视剧集的详细信息 fn tmdb_tv_season_episode_detail(tmdb_id: i64, season_number: i64, episode_number: i64) -> Result { - tracing::info!("search tv tmdb_id = {:?} season = {:?} episode = {:?}", tmdb_id, season_number, episode_number); + tracing::debug!( + "tmdb_tv_season_episode_detail tmdb_id = {:?} season = {:?} episode = {:?}", + tmdb_id, + season_number, + episode_number + ); return tv::tv_season_episode_detail(tmdb_id, season_number, episode_number); } /// 查询电视剧季的详细信息 fn tmdb_tv_season_detail(tmdb_id: i64, season_number: i64) -> Result { - tracing::info!("search tv tmdb_id = {:?} season = {:?}", tmdb_id, season_number); + tracing::debug!("tmdb_tv_season_detail tmdb_id = {:?} season = {:?}", tmdb_id, season_number); return tv::tv_season_detail(tmdb_id, season_number); } /// 查询电视剧详细信息 fn tmdb_tv_details(tmdb_id: &str) -> Result { - tracing::info!("search tv tmdb_id = {:?}", tmdb_id); + tracing::debug!("tmdb_tv_details tmdb_id = {:?}", tmdb_id); return tv::tv_details(tmdb_id); } -#[cfg(test)] -mod tmdb_tests { - - use super::*; - use crate::soda::*; - use tracing_subscriber::fmt::format::FmtSpan; - use tracing_subscriber::EnvFilter; - - #[test] - fn test_recognize_mt_1() { - init_tracing(); - let mut metadata = meta::mt_metadata("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC-OurTV.mp4").unwrap(); - let value = tmdb::recognize_mt(&mut metadata).unwrap(); - assert_eq!("凡人修仙传", value.original_title()); - } - - /// 初始化日志配置 - fn init_tracing() { - tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env().add_directive("soda=info".parse().unwrap())).with_span_events(FmtSpan::FULL).init(); - } +/// 查询电影详细信息 +fn tmdb_movie_details(tmdb_id: &str) -> Result { + tracing::debug!("tmdb_movie_details tmdb_id = {:?}", tmdb_id); + return movie::movie_details(tmdb_id); } + +#[cfg(test)] +mod tmdb_tests {} diff --git a/soda_resource_tools_lib/src/soda/tmdb/entity.rs b/soda_resource_tools_lib/src/soda/tmdb/entity.rs index fb3c7d85..392cb418 100644 --- a/soda_resource_tools_lib/src/soda/tmdb/entity.rs +++ b/soda_resource_tools_lib/src/soda/tmdb/entity.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use crate::soda::fanart::entity::FanartMovie; use crate::soda::fanart::entity::FanartTV; use serde::Deserialize; use serde::Serialize; @@ -12,20 +13,27 @@ pub struct TmdbSeasonInfo { } impl TmdbSeasonInfo { pub(crate) fn new(tmdb_season: TmdbSeason) -> TmdbSeasonInfo { - return TmdbSeasonInfo { tv_season: tmdb_season, tv_episodes: HashMap::new() }; + return TmdbSeasonInfo { + tv_season: tmdb_season, + tv_episodes: HashMap::new(), + }; } } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TmdbTVInfo { pub tv: TmdbTV, - pub fanart_tv: Option, + pub fanart: Option, pub tv_seasons: HashMap, } impl TmdbTVInfo { pub(crate) fn new(tmdb_tv: TmdbTV) -> TmdbTVInfo { - return TmdbTVInfo { tv: tmdb_tv, tv_seasons: HashMap::new(), fanart_tv: None }; + return TmdbTVInfo { + tv: tmdb_tv, + tv_seasons: HashMap::new(), + fanart: None, + }; } } @@ -42,11 +50,13 @@ pub struct TmdbTV { pub popularity: Option, pub poster_path: Option, pub seasons: Option>, + pub number_of_seasons: Option, pub vote_average: Option, pub vote_count: Option, pub credits: Option, pub external_ids: Option, } + impl TmdbTV { pub(crate) fn new(tmdb_info: Value) -> TmdbTV { let result: Result = serde_json::from_value(tmdb_info); @@ -55,16 +65,7 @@ impl TmdbTV { return result; } Err(e) => { - // 打印错误信息 - if e.is_data() { - tracing::error!("TmdbTV new 数据类型错误 {}", e); - } else if e.is_syntax() { - tracing::error!("TmdbTV new 语法错误 {}", e); - } else if e.is_io() { - tracing::error!("TmdbTV new IO 错误 {}", e); - } else if e.is_eof() { - tracing::error!("TmdbTV new 意外的文件结束 {}", e); - } + tracing::error!("TmdbTV new err {}", e); unreachable!("解析失败"); } } @@ -111,7 +112,10 @@ impl TmdbTV { } pub(crate) fn year(&self) -> &str { - &(self.first_air_date()[0..4]) + if self.first_air_date().is_empty() { + return ""; + } + return &(self.first_air_date()[0..4]); } pub(crate) fn tmdb_id(&self) -> String { @@ -441,3 +445,176 @@ impl TmdbSeason { } } } + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TmdbMovie { + pub id: i64, + pub imdb_id: Option, + pub release_date: Option, + pub genres: Option>, + pub title: Option, + pub original_title: Option, + pub original_language: Option, + pub overview: Option, + pub popularity: Option, + pub backdrop_path: Option, + pub poster_path: Option, + pub vote_average: Option, + pub vote_count: Option, + pub credits: Option, + pub external_ids: Option, +} +impl TmdbMovie { + pub(crate) fn new(tmdb_info: Value) -> TmdbMovie { + let result: Result = serde_json::from_value(tmdb_info); + match result { + Ok(result) => { + return result; + } + Err(e) => { + tracing::error!("TmdbMovie new err {}", e); + unreachable!("解析失败"); + } + } + } + + pub(crate) fn actors(&self) -> Option> { + let mut ret = Vec::new(); + match &self.credits { + Some(credits) => match &credits.cast { + Some(casts) => { + for cast in casts { + match cast.known_for_department() { + "Acting" => { + ret.push(cast); + } + _ => {} + } + } + } + None => return None, + }, + None => return None, + } + return Some(ret); + } + + pub(crate) fn directors(&self) -> Option> { + let mut ret = Vec::new(); + match &self.credits { + Some(credits) => match &credits.crew { + Some(crews) => { + for crew in crews { + let job = crew.job(); + if job == "Director" || job == "Writer" || job == "Editor" || job == "Producer" { + ret.push(crew); + } + } + } + None => return None, + }, + None => return None, + } + return Some(ret); + } + + pub(crate) fn year(&self) -> &str { + if self.first_air_date().is_empty() { + return ""; + } + return &(self.first_air_date()[0..4]); + } + + pub(crate) fn tmdb_id(&self) -> String { + self.id.to_string() + } + + pub(crate) fn tvdb_id(&self) -> Option { + if let Some(external_ids) = &self.external_ids { + if let Some(tvdb_id) = external_ids.tvdb_id { + return Some(tvdb_id.to_string()); + } + } + return None; + } + + pub(crate) fn imdb_id(&self) -> Option<&str> { + if let Some(external_ids) = &self.external_ids { + if let Some(imdb_id) = &external_ids.imdb_id { + return Some(imdb_id); + } + } + return None; + } + + pub(crate) fn overview(&self) -> &str { + match &self.overview { + Some(overview) => overview.as_str(), + None => "", + } + } + + pub(crate) fn name(&self) -> &str { + match &self.title { + Some(name) => name.as_str(), + None => "", + } + } + + pub(crate) fn original_language(&self) -> &str { + match &self.original_language { + Some(original_language) => original_language.as_str(), + None => "", + } + } + + pub(crate) fn first_air_date(&self) -> &str { + match &self.release_date { + Some(first_air_date) => first_air_date.as_str(), + None => "", + } + } + + pub(crate) fn poster_path(&self) -> &str { + match &self.poster_path { + Some(poster_path) => poster_path.as_str(), + None => "", + } + } + + pub(crate) fn backdrop_path(&self) -> &str { + match &self.backdrop_path { + Some(backdrop_path) => backdrop_path.as_str(), + None => "", + } + } + + pub(crate) fn original_name(&self) -> &str { + match &self.original_title { + Some(original_name) => original_name.as_str(), + None => "", + } + } + + pub(crate) fn vote_average(&self) -> String { + match &self.vote_average { + Some(vote_average) => vote_average.to_string(), + None => "".to_string(), + } + } +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TmdbMovieInfo { + pub movie: TmdbMovie, + pub fanart: Option, +} + +impl TmdbMovieInfo { + pub(crate) fn new(tmdb_movie: TmdbMovie) -> TmdbMovieInfo { + return TmdbMovieInfo { + movie: tmdb_movie, + fanart: None, + }; + } +} diff --git a/soda_resource_tools_lib/src/soda/tmdb/movie.rs b/soda_resource_tools_lib/src/soda/tmdb/movie.rs new file mode 100644 index 00000000..1d97c3d7 --- /dev/null +++ b/soda_resource_tools_lib/src/soda/tmdb/movie.rs @@ -0,0 +1,165 @@ +use std::f32::consts::E; + +use reqwest::Response; +use serde_json::Value; + +use crate::soda::entity::SodaError; + +use super::{ + entity::{TmdbEpisode, TmdbMovie, TmdbSeason, TmdbTV}, + request, +}; + +/// https://developer.themoviedb.org/reference/movie-details +/// https://api.themoviedb.org/3/movie/{movie_id} +/// ```json +/// { +/// "adult": false, +/// "backdrop_path": "/4HodYYKEIsGOdinkGi2Ucz6X9i0.jpg", +/// "belongs_to_collection": { +/// "id": 573436, +/// "name": "蜘蛛侠:平行宇宙(系列)", +/// "poster_path": "/eD4bGQNfmqExIAzKdvX5gDHhI2.jpg", +/// "backdrop_path": "/oC5S5pdfzPiQC4us05uDMT7v5Ng.jpg" +/// }, +/// "budget": 100000000, +/// "genres": [ +/// { +/// "id": 16, +/// "name": "动画" +/// }, +/// { +/// "id": 28, +/// "name": "动作" +/// }, +/// { +/// "id": 12, +/// "name": "冒险" +/// }, +/// { +/// "id": 878, +/// "name": "科幻" +/// } +/// ], +/// "homepage": "", +/// "id": 569094, +/// "imdb_id": "tt9362722", +/// "original_language": "en", +/// "original_title": "Spider-Man: Across the Spider-Verse", +/// "overview": "讲述了新生代蜘蛛侠迈尔斯(沙梅克·摩尔 Shameik Moore 配音)携手蜘蛛格温(海莉·斯坦菲尔德 Hailee Steinfeld 配音),穿越多元宇宙踏上更宏大的冒险征程的故事。面临每个蜘蛛侠都会失去至亲的宿命,迈尔斯誓言打破命运魔咒,找到属于自己的英雄之路。而这个决定和蜘蛛侠2099(奥斯卡·伊萨克 Oscar Is aac 配音)所领军的蜘蛛联盟产生了极大冲突,一场以一敌百的蜘蛛侠大内战即将拉响!", +/// "popularity": 357.459, +/// "poster_path": "/jBmJZDiJ2h6DimGROpxWlPh1xIo.jpg", +/// "production_companies": [ +/// { +/// "id": 5, +/// "logo_path": "/71BqEFAF4V3qjjMPCpLuyJFB9A.png", +/// "name": "Columbia Pictures", +/// "origin_country": "US" +/// }, +/// { +/// "id": 2251, +/// "logo_path": "/5ilV5mH3gxTEU7p5wjxptHvXkyr.png", +/// "name": "Sony Pictures Animation", +/// "origin_country": "US" +/// }, +/// { +/// "id": 77973, +/// "logo_path": "/9y5lW86HnxKUZOFencYk3TIIRCM.png", +/// "name": "Lord Miller", +/// "origin_country": "US" +/// }, +/// { +/// "id": 84041, +/// "logo_path": "/nw4kyc29QRpNtFbdsBHkRSFavvt.png", +/// "name": "Pascal Pictures", +/// "origin_country": "US" +/// }, +/// { +/// "id": 14439, +/// "logo_path": null, +/// "name": "Arad Productions", +/// "origin_country": "US" +/// }, +/// { +/// "id": 7505, +/// "logo_path": "/837VMM4wOkODc1idNxGT0KQJlej.png", +/// "name": "Marvel Entertainment", +/// "origin_country": "US" +/// } +/// ], +/// "production_countries": [ +/// { +/// "iso_3166_1": "US", +/// "name": "United States of America" +/// } +/// ], +/// "release_date": "2023-05-31", +/// "revenue": 690500000, +/// "runtime": 140, +/// "spoken_languages": [ +/// { +/// "english_name": "English", +/// "iso_639_1": "en", +/// "name": "English" +/// }, +/// { +/// "english_name": "Hindi", +/// "iso_639_1": "hi", +/// "name": "हिन्दी" +/// }, +/// { +/// "english_name": "Italian", +/// "iso_639_1": "it", +/// "name": "Italiano" +/// }, +/// { +/// "english_name": "Spanish", +/// "iso_639_1": "es", +/// "name": "Español" +/// } +/// ], +/// "status": "Released", +/// "tagline": "", +/// "title": "蜘蛛侠:纵横宇宙", +/// "video": false, +/// "vote_average": 8.376, +/// "vote_count": 5554, +/// "credits": { +/// "cast": [ +/// { +/// "adult": false, +/// "gender": 2, +/// "id": 587506, +/// "known_for_department": "Acting", +/// "name": "Shameik Moore", +/// "original_name": "Shameik Moore", +/// "popularity": 22.61, +/// "profile_path": "/uJNaSTsfBOvtFWsPP23zNthknsB.jpg", +/// "cast_id": 705, +/// "character": "Miles Morales / Spider-Man (voice)", +/// "credit_id": "6489a4f8e375c000e251ab48", +/// "order": 0 +/// } +/// ] +/// }, +/// "external_ids": { +/// "imdb_id": "tt9362722", +/// "wikidata_id": "Q76448600", +/// "facebook_id": "SpiderVerseMovie", +/// "instagram_id": "spiderversemovie", +/// "twitter_id": "SpiderVerse" +/// } +/// } +/// ``` +pub(crate) fn movie_details(tmdb_id: &str) -> Result { + let action = format!("/movie/{}", tmdb_id); + + let params = format!("append_to_response={}", "credits,external_ids"); + + let response = request::tmdb_request(&action, ¶ms, "GET")?; + + match serde_json::from_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(SodaError::String(format!("movie_details json error {:?}", e))), + } +} diff --git a/soda_resource_tools_lib/src/soda/tmdb/request.rs b/soda_resource_tools_lib/src/soda/tmdb/request.rs index 5c0c2b09..a8d93fb6 100644 --- a/soda_resource_tools_lib/src/soda/tmdb/request.rs +++ b/soda_resource_tools_lib/src/soda/tmdb/request.rs @@ -7,15 +7,24 @@ use crate::soda::request::blocking_request; use crate::soda::{cache, request, utils}; pub(crate) fn tmdb_request(action: &str, params: &str, method: &str) -> Result { - tracing::info!("action = {} params = {} method = {}", action, params, method); - let api_key = get_api_key() .on_none_inspect(|| { - tracing::info!("TheMovieDb API Key 未设置"); + tracing::debug!("TheMovieDb API Key 未设置"); }) .unwrap(); - let url = if params.contains("language") { format!("https://{}/3{}?api_key={}&{}", get_api_domain(), action, api_key, params) } else { format!("https://{}/3{}?api_key={}&{}&language={}", get_api_domain(), action, api_key, params, get_api_language()) }; + let url = if params.contains("language") { + format!("https://{}/3{}?api_key={}&{}", get_api_domain(), action, api_key, params) + } else { + format!( + "https://{}/3{}?api_key={}&{}&language={}", + get_api_domain(), + action, + api_key, + params, + get_api_language() + ) + }; let json = request::blocking_request_value_with_cache(cache::CacheType::TMDB, method, &url)?; diff --git a/soda_resource_tools_lib/src/soda/tmdb/scraper.rs b/soda_resource_tools_lib/src/soda/tmdb/scraper.rs index 5a0436da..700ece33 100644 --- a/soda_resource_tools_lib/src/soda/tmdb/scraper.rs +++ b/soda_resource_tools_lib/src/soda/tmdb/scraper.rs @@ -1,61 +1,81 @@ use std::{ops::Index, path::Path}; -use tracing_subscriber::fmt::format; use xml::{writer::XmlEvent, EmitterConfig, EventWriter}; use crate::soda::{ dom, entity::*, extension_option::OptionExtensions, - fanart::entity::FanartTV, + fanart::entity::{FanartMovie, FanartTV}, request, utils::{self, str_replace_extension, time}, }; -use super::entity::{TmdbEpisode, TmdbSeason, TmdbTV}; +use super::entity::{TmdbEpisode, TmdbMovie, TmdbSeason, TmdbTV}; pub(crate) fn gen_scrape_files(scrape_config: &ScrapeConfig, mt_meta: &MTMetadata, mt_info: &MTInfo, path: &str) { match mt_info { - MTInfo::MOVIE(_) => todo!(), MTInfo::TV(TVType::TMDB(tmdb_tv_info)) => { gen_scrape_tv_files(scrape_config, path, tmdb_tv_info, mt_meta); } + MTInfo::MOVIE(MovieType::TMDB(tmdb_movie_info)) => { + gen_scrape_movie_files(scrape_config, path, tmdb_movie_info, mt_meta); + } } } -fn gen_scrape_tv_files(scrape_config: &ScrapeConfig, path: &str, tmdb_tv_info: &super::entity::TmdbTVInfo, meta: &MTMetadata) { +fn gen_scrape_movie_files(scrape_config: &ScrapeConfig, path: &str, tmdb_info: &super::entity::TmdbMovieInfo, mt_meta: &MTMetadata) { // root - let tv_root_path = Path::new(path).parent().unwrap().parent().unwrap(); + let root_path = Path::new(path).parent().unwrap(); + + // gen movie.nfo file + if !root_path.join("movie.nfo").exists() { + gen_movie_nfo_file(&tmdb_info.movie, root_path, Path::new(path)); + } else { + tracing::debug!("movie.nfo file exist, skip gen tvshow.nfo file"); + } + + if scrape_config.enable_scrape_image { + // save movie images + tmdb_info.fanart.is_some_then(|fanart| { + save_movie_images(&tmdb_info.movie, fanart, root_path); + }); + } +} + +fn gen_scrape_tv_files(scrape_config: &ScrapeConfig, path: &str, tmdb_info: &super::entity::TmdbTVInfo, meta: &MTMetadata) { + // root + let root_path = Path::new(path).parent().unwrap().parent().unwrap(); // gen tvshow.nfo file - if !tv_root_path.join("tvshow.nfo").exists() { - gen_tvshow_nfo_file(&tmdb_tv_info.tv, tv_root_path); + if !root_path.join("tvshow.nfo").exists() { + gen_tvshow_nfo_file(&tmdb_info.tv, root_path); } else { - tracing::info!("tvshow.nfo file exist, skip gen tvshow.nfo file"); + tracing::debug!("tvshow.nfo file exist, skip gen tvshow.nfo file"); } if scrape_config.enable_scrape_image { // save tv images - tmdb_tv_info.fanart_tv.is_some_then(|fanart_tv| { - save_tv_show_images(&tmdb_tv_info.tv, fanart_tv, tv_root_path); + tmdb_info.fanart.is_some_then(|fanart_tv| { + save_tv_show_images(&tmdb_info.tv, fanart_tv, root_path); }); } // season meta.season_number().is_some_then(|season_number| { - tmdb_tv_info.tv_seasons.get(season_number).is_some_then(|season_info| { + tmdb_info.tv_seasons.get(season_number).is_some_then(|season_info| { let tv_season_path = Path::new(path).parent().unwrap(); // gen season.nfo file if !tv_season_path.join("season.nfo").exists() { gen_season_nfo_file(meta, &season_info.tv_season, tv_season_path); } else { - tracing::info!("season.nfo file exist, skip gen season.nfo file"); + tracing::debug!("season.nfo file exist, skip gen season.nfo file"); } if scrape_config.enable_scrape_image { // save season images - save_season_images(&season_info.tv_season, tv_root_path, tv_season_path); + save_season_images(&season_info.tv_season, root_path, tv_season_path); } }); }); @@ -63,7 +83,7 @@ fn gen_scrape_tv_files(scrape_config: &ScrapeConfig, path: &str, tmdb_tv_info: & // episode meta.season_number().is_some_then(|season_number| { meta.episode_number().is_some_then(|episode_number| { - tmdb_tv_info.tv_seasons.get(season_number).is_some_then(|season_info| { + tmdb_info.tv_seasons.get(season_number).is_some_then(|season_info| { season_info.tv_episodes.get(episode_number).is_some_then(|episode| { let tv_episode_path = Path::new(path); @@ -76,7 +96,7 @@ fn gen_scrape_tv_files(scrape_config: &ScrapeConfig, path: &str, tmdb_tv_info: & save_episode_images(episode, tv_episode_path); } } else { - tracing::info!("episode file not exist, skip gen episode.nfo file"); + tracing::debug!("episode file not exist, skip gen episode.nfo file"); } }); }); @@ -85,166 +105,298 @@ fn gen_scrape_tv_files(scrape_config: &ScrapeConfig, path: &str, tmdb_tv_info: & } fn save_episode_images(episode: &TmdbEpisode, tv_episode_path: &Path) { - tracing::info!("save episode images, tv_episode_path = {:?}", tv_episode_path); + tracing::debug!("save episode images, tv_episode_path = {:?}", tv_episode_path); episode.still_path.is_some_then(|still_path| { let suffix = still_path.split(".").last().unwrap(); let image_path = tv_episode_path.clone().with_extension(suffix); if !image_path.exists() { - tracing::info!("save still image, image_path = {:?}", image_path); + tracing::debug!("save still image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(&format!("https://image.tmdb.org/t/p/original{}", still_path), &image_path); } else { - tracing::info!("still image exist, skip save still image, image_path = {:?}", image_path); + tracing::debug!("still image exist, skip save still image, image_path = {:?}", image_path); } }); } fn save_season_images(tv_season: &TmdbSeason, tv_root_path: &Path, tv_season_path: &Path) { - tracing::info!("save season images, tv_season_path = {:?}", tv_season_path); + tracing::debug!("save season images, tv_season_path = {:?}", tv_season_path); tv_season.season_number.is_some_then(|season_number| { tv_season.poster_path.is_some_then(|poster_path| { let suffix = poster_path.split(".").last().unwrap(); let url = format!("https://image.tmdb.org/t/p/original{}", poster_path); let image_path = tv_root_path.join(format!("season{:02}-poster.{}", season_number, suffix)); if !image_path.exists() { - tracing::info!("save poster image, image_path = {:?}", image_path); + tracing::debug!("save poster image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(&url, &image_path); } else { - tracing::info!("poster image exist, skip save poster image, image_path = {:?}", image_path); + tracing::debug!("poster image exist, skip save poster image, image_path = {:?}", image_path); } }); }); } -fn save_tv_show_images(tv: &TmdbTV, fanart_tv: &FanartTV, tv_root_path: &Path) { - tracing::info!("save tv show images, tv_root_path = {:?}", tv_root_path); +fn save_movie_images(tmdb: &TmdbMovie, fanart: &FanartMovie, root_path: &Path) { + tracing::debug!("save movie images, root_path = {:?}", root_path); // banner - if let Some(banner) = &fanart_tv.tvbanner { + if let Some(banner) = &fanart.moviebanner { if let Some(image) = banner.first() { let url = image.url(); if !url.is_empty() { let suffix = url.split(".").last().unwrap(); - let image_path = tv_root_path.join(format!("banner.{}", suffix)); + let image_path = root_path.join(format!("banner.{}", suffix)); + if !image_path.exists() { + tracing::debug!("save banner image, image_path = {:?}", image_path); + request::blocking_get_request_and_download_file(url, &image_path); + } else { + tracing::debug!("banner image exist, skip save banner image, image_path = {:?}", image_path); + } + } + } + } + + // logo + if let Some(logo) = &fanart.hdmovielogo { + if let Some(image) = logo.first() { + let url = image.url(); + if !url.is_empty() { + let suffix = url.split(".").last().unwrap(); + let image_path = root_path.join(format!("logo.{}", suffix)); + if !image_path.exists() { + tracing::debug!("save logo image, image_path = {:?}", image_path); + request::blocking_get_request_and_download_file(url, &image_path); + } else { + tracing::debug!("logo image exist, skip save logo image, image_path = {:?}", image_path); + } + } + } + } + + // thumb + if let Some(thumb) = &fanart.moviethumb { + if let Some(image) = thumb.first() { + let url = image.url(); + if !url.is_empty() { + let suffix = url.split(".").last().unwrap(); + let image_path = root_path.join(format!("thumb.{}", suffix)); + if !image_path.exists() { + tracing::debug!("save thumb image, image_path = {:?}", image_path); + request::blocking_get_request_and_download_file(url, &image_path); + } else { + tracing::debug!("thumb image exist, skip save thumb image, image_path = {:?}", image_path); + } + } + } + } + + // background + if let Some(background) = &fanart.moviebackground { + if let Some(image) = background.first() { + let url = image.url(); + if !url.is_empty() { + let suffix = url.split(".").last().unwrap(); + let image_path = root_path.join(format!("background.{}", suffix)); + if !image_path.exists() { + tracing::debug!("save background image, image_path = {:?}", image_path); + request::blocking_get_request_and_download_file(url, &image_path); + } else { + tracing::debug!("background image exist, skip save background image, image_path = {:?}", image_path); + } + } + } + } + + // disc + if let Some(disc) = &fanart.moviedisc { + if let Some(image) = disc.first() { + let url = image.url(); + if !url.is_empty() { + let suffix = url.split(".").last().unwrap(); + let image_path = root_path.join(format!("disc.{}", suffix)); + if !image_path.exists() { + tracing::debug!("save disc image, image_path = {:?}", image_path); + request::blocking_get_request_and_download_file(url, &image_path); + } else { + tracing::debug!("disc image exist, skip save disc image, image_path = {:?}", image_path); + } + } + } + } + + // clearart + if let Some(clearart) = &fanart.hdmovieclearart { + if let Some(image) = clearart.first() { + let url = image.url(); + if !url.is_empty() { + let suffix = url.split(".").last().unwrap(); + let image_path = root_path.join(format!("clearart.{}", suffix)); if !image_path.exists() { - tracing::info!("save banner image, image_path = {:?}", image_path); + tracing::debug!("save clearart image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(url, &image_path); } else { - tracing::info!("banner image exist, skip save banner image, image_path = {:?}", image_path); + tracing::debug!("clearart image exist, skip save clearart image, image_path = {:?}", image_path); + } + } + } + } + + // poster + if !tmdb.poster_path().is_empty() { + let poster_path = tmdb.poster_path(); + let suffix = poster_path.split(".").last().unwrap(); + let image_path = root_path.join(format!("poster.{}", suffix)); + if !image_path.exists() { + tracing::debug!("save poster image, image_path = {:?}", image_path); + request::blocking_get_request_and_download_file(&format!("https://image.tmdb.org/t/p/original{}", poster_path), &image_path); + } else { + tracing::debug!("poster image exist, skip save poster image, image_path = {:?}", image_path); + } + } + + // backdrop + if !tmdb.backdrop_path().is_empty() { + let backdrop_path = tmdb.backdrop_path(); + let suffix = backdrop_path.split(".").last().unwrap(); + let image_path = root_path.join(format!("backdrop.{}", suffix)); + if !image_path.exists() { + tracing::debug!("save backdrop image, image_path = {:?}", image_path); + request::blocking_get_request_and_download_file(&format!("https://image.tmdb.org/t/p/original{}", backdrop_path), &image_path); + } else { + tracing::debug!("backdrop image exist, skip save backdrop image, image_path = {:?}", image_path); + } + } +} + +fn save_tv_show_images(tmdb: &TmdbTV, fanart: &FanartTV, root_path: &Path) { + tracing::debug!("save tv show images, tv_root_path = {:?}", root_path); + + // banner + if let Some(banner) = &fanart.tvbanner { + if let Some(image) = banner.first() { + let url = image.url(); + if !url.is_empty() { + let suffix = url.split(".").last().unwrap(); + let image_path = root_path.join(format!("banner.{}", suffix)); + if !image_path.exists() { + tracing::debug!("save banner image, image_path = {:?}", image_path); + request::blocking_get_request_and_download_file(url, &image_path); + } else { + tracing::debug!("banner image exist, skip save banner image, image_path = {:?}", image_path); } } } } // characterart - if let Some(characterart) = &fanart_tv.characterart { + if let Some(characterart) = &fanart.characterart { if let Some(image) = characterart.first() { let url = image.url(); if !url.is_empty() { let suffix = url.split(".").last().unwrap(); - let image_path = tv_root_path.join(format!("characterart.{}", suffix)); + let image_path = root_path.join(format!("characterart.{}", suffix)); if !image_path.exists() { - tracing::info!("save characterart image, image_path = {:?}", image_path); + tracing::debug!("save characterart image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(url, &image_path); } else { - tracing::info!("characterart image exist, skip save characterart image, image_path = {:?}", image_path); + tracing::debug!("characterart image exist, skip save characterart image, image_path = {:?}", image_path); } } } } // logo - if let Some(logo) = &fanart_tv.hdtvlogo { + if let Some(logo) = &fanart.hdtvlogo { if let Some(image) = logo.first() { let url = image.url(); if !url.is_empty() { let suffix = url.split(".").last().unwrap(); - let image_path = tv_root_path.join(format!("logo.{}", suffix)); + let image_path = root_path.join(format!("logo.{}", suffix)); if !image_path.exists() { - tracing::info!("save logo image, image_path = {:?}", image_path); + tracing::debug!("save logo image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(url, &image_path); } else { - tracing::info!("logo image exist, skip save logo image, image_path = {:?}", image_path); + tracing::debug!("logo image exist, skip save logo image, image_path = {:?}", image_path); } } } } // thumb - if let Some(thumb) = &fanart_tv.tvthumb { + if let Some(thumb) = &fanart.tvthumb { if let Some(image) = thumb.first() { let url = image.url(); if !url.is_empty() { let suffix = url.split(".").last().unwrap(); - let image_path = tv_root_path.join(format!("thumb.{}", suffix)); + let image_path = root_path.join(format!("thumb.{}", suffix)); if !image_path.exists() { - tracing::info!("save thumb image, image_path = {:?}", image_path); + tracing::debug!("save thumb image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(url, &image_path); } else { - tracing::info!("thumb image exist, skip save thumb image, image_path = {:?}", image_path); + tracing::debug!("thumb image exist, skip save thumb image, image_path = {:?}", image_path); } } } } // background - if let Some(background) = &fanart_tv.showbackground { + if let Some(background) = &fanart.showbackground { if let Some(image) = background.first() { let url = image.url(); if !url.is_empty() { let suffix = url.split(".").last().unwrap(); - let image_path = tv_root_path.join(format!("background.{}", suffix)); + let image_path = root_path.join(format!("background.{}", suffix)); if !image_path.exists() { - tracing::info!("save background image, image_path = {:?}", image_path); + tracing::debug!("save background image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(url, &image_path); } else { - tracing::info!("background image exist, skip save background image, image_path = {:?}", image_path); + tracing::debug!("background image exist, skip save background image, image_path = {:?}", image_path); } } } } // clearart - if let Some(clearart) = &fanart_tv.hdclearart { + if let Some(clearart) = &fanart.hdclearart { if let Some(image) = clearart.first() { let url = image.url(); if !url.is_empty() { let suffix = url.split(".").last().unwrap(); - let image_path = tv_root_path.join(format!("clearart.{}", suffix)); + let image_path = root_path.join(format!("clearart.{}", suffix)); if !image_path.exists() { - tracing::info!("save clearart image, image_path = {:?}", image_path); + tracing::debug!("save clearart image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(url, &image_path); } else { - tracing::info!("clearart image exist, skip save clearart image, image_path = {:?}", image_path); + tracing::debug!("clearart image exist, skip save clearart image, image_path = {:?}", image_path); } } } } // poster - if !tv.poster_path().is_empty() { - let poster_path = tv.poster_path(); + if !tmdb.poster_path().is_empty() { + let poster_path = tmdb.poster_path(); let suffix = poster_path.split(".").last().unwrap(); - let image_path = tv_root_path.join(format!("poster.{}", suffix)); + let image_path = root_path.join(format!("poster.{}", suffix)); if !image_path.exists() { - tracing::info!("save poster image, image_path = {:?}", image_path); + tracing::debug!("save poster image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(&format!("https://image.tmdb.org/t/p/original{}", poster_path), &image_path); } else { - tracing::info!("poster image exist, skip save poster image, image_path = {:?}", image_path); + tracing::debug!("poster image exist, skip save poster image, image_path = {:?}", image_path); } } // backdrop - if !tv.backdrop_path().is_empty() { - let backdrop_path = tv.backdrop_path(); + if !tmdb.backdrop_path().is_empty() { + let backdrop_path = tmdb.backdrop_path(); let suffix = backdrop_path.split(".").last().unwrap(); - let image_path = tv_root_path.join(format!("backdrop.{}", suffix)); + let image_path = root_path.join(format!("backdrop.{}", suffix)); if !image_path.exists() { - tracing::info!("save backdrop image, image_path = {:?}", image_path); + tracing::debug!("save backdrop image, image_path = {:?}", image_path); request::blocking_get_request_and_download_file(&format!("https://image.tmdb.org/t/p/original{}", backdrop_path), &image_path); } else { - tracing::info!("backdrop image exist, skip save backdrop image, image_path = {:?}", image_path); + tracing::debug!("backdrop image exist, skip save backdrop image, image_path = {:?}", image_path); } } @@ -254,10 +406,10 @@ fn save_tv_show_images(tv: &TmdbTV, fanart_tv: &FanartTV, tv_root_path: &Path) { // let suffix = image.url.split(".").last().unwrap(); // let image_path = tv_root_path.join(format!("poster.{}", suffix)); // if !image_path.exists() { - // tracing::info!("save poster image, image_path = {:?}", image_path); + // tracing::debug!("save poster image, image_path = {:?}", image_path); // utils::download_file(&image.url, &image_path); // } else { - // tracing::info!("poster image exist, skip save poster image, image_path = {:?}", image_path); + // tracing::debug!("poster image exist, skip save poster image, image_path = {:?}", image_path); // } // }); @@ -269,10 +421,10 @@ fn save_tv_show_images(tv: &TmdbTV, fanart_tv: &FanartTV, tv_root_path: &Path) { // Ok(season_number) => { // let image_path = tv_root_path.join(format!("season{:02}-poster.{}", season_number, suffix)); // if !image_path.exists() { - // tracing::info!("save season image, image_path = {:?}", image_path); + // tracing::debug!("save season image, image_path = {:?}", image_path); // utils::download_file(&image.url, &image_path); // } else { - // tracing::info!("season image exist, skip save season image, image_path = {:?}", image_path); + // tracing::debug!("season image exist, skip save season image, image_path = {:?}", image_path); // } // } // Err(e) => { @@ -305,12 +457,16 @@ fn save_tv_show_images(tv: &TmdbTV, fanart_tv: &FanartTV, tv_root_path: &Path) { fn gen_episode_nfo_file(meta: &MTMetadata, season_number: &i64, episode_number: &i64, tmdb_episode: &TmdbEpisode, tv_episode_path: &Path) { let tv_episode_path = tv_episode_path.clone().with_extension("nfo"); - tracing::info!("gen episode.nfo file, tv_episode_path = {:?}", tv_episode_path); + tracing::debug!("gen episode.nfo file, tv_episode_path = {:?}", tv_episode_path); let mut xml: Vec = Vec::new(); // - let mut w = EmitterConfig::new().write_document_declaration(true).pad_self_closing(true).perform_indent(true).create_writer(&mut xml); + let mut w = EmitterConfig::new() + .write_document_declaration(true) + .pad_self_closing(true) + .perform_indent(true) + .create_writer(&mut xml); // season // root @@ -321,7 +477,8 @@ fn gen_episode_nfo_file(meta: &MTMetadata, season_number: &i64, episode_number: dom::write_text_element(&mut w, "dateadded", &time::now_time_format()); // uniqueid - w.write(XmlEvent::start_element("uniqueid").attr("type", "tmdb").attr("default", "true")).unwrap(); + w.write(XmlEvent::start_element("uniqueid").attr("type", "tmdb").attr("default", "true")) + .unwrap(); w.write(XmlEvent::characters(&tmdb_episode.tmdb_id_str())).unwrap(); w.write(XmlEvent::end_element()).unwrap(); @@ -360,7 +517,7 @@ fn gen_episode_nfo_file(meta: &MTMetadata, season_number: &i64, episode_number: dom::save_nfo(&xml, tv_episode_path.to_str().unwrap()); - tracing::info!("gen episode.nfo file, tv_episode_path = {:?} success", tv_episode_path); + tracing::debug!("gen episode.nfo file, tv_episode_path = {:?} success", tv_episode_path); } // @@ -375,12 +532,16 @@ fn gen_episode_nfo_file(meta: &MTMetadata, season_number: &i64, episode_number: // 12 // fn gen_season_nfo_file(meta: &MTMetadata, season: &TmdbSeason, tv_season_path: &Path) { - tracing::info!("gen season.nfo file, title = {:?} tv_season_path = {:?}", season.title(), tv_season_path); + tracing::debug!("gen season.nfo file, title = {:?} tv_season_path = {:?}", season.title(), tv_season_path); let mut xml: Vec = Vec::new(); // - let mut w = EmitterConfig::new().write_document_declaration(true).pad_self_closing(true).perform_indent(true).create_writer(&mut xml); + let mut w = EmitterConfig::new() + .write_document_declaration(true) + .pad_self_closing(true) + .perform_indent(true) + .create_writer(&mut xml); // season // root @@ -411,7 +572,11 @@ fn gen_season_nfo_file(meta: &MTMetadata, season: &TmdbSeason, tv_season_path: & // save nfo dom::save_nfo(&xml, tv_season_path.join("season.nfo").to_str().unwrap()); - tracing::info!("gen season.nfo file, title = {:?} tv_season_path = {:?} success", season.title(), tv_season_path); + tracing::debug!( + "gen season.nfo file, title = {:?} tv_season_path = {:?} success", + season.title(), + tv_season_path + ); } // @@ -479,31 +644,35 @@ fn gen_season_nfo_file(meta: &MTMetadata, season: &TmdbSeason, tv_season_path: & // -1 // -1 // -fn gen_tvshow_nfo_file(tmdb_tv: &TmdbTV, tv_root_path: &Path) { - tracing::info!("gen tvshow.nfo file, title = {:?} tv_root_path = {:?}", tmdb_tv.name, tv_root_path); +fn gen_tvshow_nfo_file(tmdb: &TmdbTV, root_path: &Path) { + tracing::debug!("gen tvshow.nfo file, title = {:?} root_path = {:?}", tmdb.name, root_path); let mut xml: Vec = Vec::new(); // - let mut w = EmitterConfig::new().write_document_declaration(true).pad_self_closing(true).perform_indent(true).create_writer(&mut xml); + let mut w = EmitterConfig::new() + .write_document_declaration(true) + .pad_self_closing(true) + .perform_indent(true) + .create_writer(&mut xml); // root w.write(XmlEvent::start_element("tvshow")).unwrap(); // gen common nfo - gen_common_nfo(&mut w, tmdb_tv); + gen_tv_common_nfo(&mut w, tmdb); // title - dom::write_text_element(&mut w, "title", tmdb_tv.name()); + dom::write_text_element(&mut w, "title", tmdb.name()); // originaltitle - dom::write_text_element(&mut w, "originaltitle", tmdb_tv.original_language()); + dom::write_text_element(&mut w, "originaltitle", tmdb.original_language()); // premiered - dom::write_text_element(&mut w, "premiered", tmdb_tv.first_air_date()); + dom::write_text_element(&mut w, "premiered", tmdb.first_air_date()); // year - dom::write_text_element(&mut w, "year", tmdb_tv.year()); + dom::write_text_element(&mut w, "year", tmdb.year()); // season dom::write_text_element(&mut w, "season", "-1"); @@ -515,29 +684,157 @@ fn gen_tvshow_nfo_file(tmdb_tv: &TmdbTV, tv_root_path: &Path) { w.write(XmlEvent::end_element()).unwrap(); // save nfo - dom::save_nfo(&xml, tv_root_path.join("tvshow.nfo").to_str().unwrap()); + dom::save_nfo(&xml, root_path.join("tvshow.nfo").to_str().unwrap()); + + tracing::debug!("gen tvshow.nfo file, title = {:?} tv_root_path = {:?} success", tmdb.name, root_path); +} + +fn gen_tv_common_nfo(w: &mut EventWriter<&mut Vec>, tmdb: &TmdbTV) { + // dateadded + // get now time and format to 2021-01-01 00:00:00 + dom::write_text_element(w, "dateadded", &time::now_time_format()); + + // TMDB + if !tmdb.tmdb_id().is_empty() { + // 1418 + dom::write_text_element(w, "tmdbid", &tmdb.tmdb_id()); + + // 1418 + w.write(XmlEvent::start_element("uniqueid").attr("type", "tmdb").attr("default", "false")) + .unwrap(); + w.write(XmlEvent::characters(&tmdb.tmdb_id())).unwrap(); + w.write(XmlEvent::end_element()).unwrap(); + } + + // TVDB + tmdb.tvdb_id().is_some_then(|tvdb_id| { + // 80379 + dom::write_text_element(w, "tvdbid", &tvdb_id.to_string()); + + // 80379 + w.write(XmlEvent::start_element("uniqueid").attr("type", "tvdb")).unwrap(); + w.write(XmlEvent::characters(&tvdb_id.to_string())).unwrap(); + w.write(XmlEvent::end_element()).unwrap(); + }); + + // IMDB + tmdb.imdb_id().is_some_then(|imdb_id| { + // tt0898266 + dom::write_text_element(w, "imdbid", imdb_id); + + // tt0898266 + w.write(XmlEvent::start_element("uniqueid").attr("type", "imdb").attr("default", "true")) + .unwrap(); + w.write(XmlEvent::characters(imdb_id)).unwrap(); + w.write(XmlEvent::end_element()).unwrap(); + }); + + // overview + // plot + dom::write_cdata_text_element(w, "plot", &tmdb.overview()); + + // outline + dom::write_cdata_text_element(w, "outline", &tmdb.overview()); + + // director + tmdb.directors().is_some_then(|directors| { + directors.iter().for_each(|director| { + w.write(XmlEvent::start_element("director").attr("tmdbid", &director.id())).unwrap(); + w.write(XmlEvent::characters(director.name())).unwrap(); + w.write(XmlEvent::end_element()).unwrap(); + }); + }); + + // actors + tmdb.actors().is_some_then(|actors| { + actors.iter().for_each(|actor| { + w.write(XmlEvent::start_element("actor")).unwrap(); + + dom::write_text_element(w, "name", actor.name()); + dom::write_text_element(w, "type", "Actor"); + dom::write_text_element(w, "role", actor.character()); + dom::write_text_element(w, "tmdbid", &actor.id()); + dom::write_text_element(w, "thumb", &actor.thumb()); + dom::write_text_element(w, "profile", &actor.profile()); + + w.write(XmlEvent::end_element()).unwrap(); + }); + }); + + // genre + tmdb.genres.is_some_then(|genres| { + genres.iter().for_each(|genre| dom::write_text_element(w, "genre", genre.name())); + }); + + // rating + dom::write_text_element(w, "rating", &tmdb.vote_average()); +} + +fn gen_movie_nfo_file(tmdb: &super::entity::TmdbMovie, root_path: &Path, movie_path: &Path) { + let movie_path = movie_path.clone().with_extension("nfo"); + + tracing::debug!( + "gen movie.nfo file, title = {:?} root_path = {:?} movie_path = {:?}", + tmdb.title, + root_path, + movie_path + ); + + let mut xml: Vec = Vec::new(); + + // + let mut w = EmitterConfig::new() + .write_document_declaration(true) + .pad_self_closing(true) + .perform_indent(true) + .create_writer(&mut xml); + + // root + w.write(XmlEvent::start_element("movie")).unwrap(); + + // gen common nfo + gen_movie_common_nfo(&mut w, tmdb); + + // title + dom::write_text_element(&mut w, "title", tmdb.name()); + + // originaltitle + dom::write_text_element(&mut w, "originaltitle", tmdb.original_language()); + + // premiered + dom::write_text_element(&mut w, "premiered", tmdb.first_air_date()); + + // year + dom::write_text_element(&mut w, "year", tmdb.year()); + + // end root + w.write(XmlEvent::end_element()).unwrap(); + + // save nfo + dom::save_nfo(&xml, movie_path.to_str().unwrap()); - tracing::info!("gen tvshow.nfo file, title = {:?} tv_root_path = {:?} success", tmdb_tv.name, tv_root_path); + tracing::debug!("gen movie.nfo file, title = {:?} tv_root_path = {:?} success", tmdb.title, root_path); } -fn gen_common_nfo(w: &mut EventWriter<&mut Vec>, tmdb_tv: &TmdbTV) { +fn gen_movie_common_nfo(w: &mut EventWriter<&mut Vec>, tmdb: &TmdbMovie) { // dateadded // get now time and format to 2021-01-01 00:00:00 dom::write_text_element(w, "dateadded", &time::now_time_format()); // TMDB - if !tmdb_tv.tmdb_id().is_empty() { + if !tmdb.tmdb_id().is_empty() { // 1418 - dom::write_text_element(w, "tmdbid", &tmdb_tv.tmdb_id()); + dom::write_text_element(w, "tmdbid", &tmdb.tmdb_id()); // 1418 - w.write(XmlEvent::start_element("uniqueid").attr("type", "tmdb").attr("default", "false")).unwrap(); - w.write(XmlEvent::characters(&tmdb_tv.tmdb_id())).unwrap(); + w.write(XmlEvent::start_element("uniqueid").attr("type", "tmdb").attr("default", "false")) + .unwrap(); + w.write(XmlEvent::characters(&tmdb.tmdb_id())).unwrap(); w.write(XmlEvent::end_element()).unwrap(); } // TVDB - tmdb_tv.tvdb_id().is_some_then(|tvdb_id| { + tmdb.tvdb_id().is_some_then(|tvdb_id| { // 80379 dom::write_text_element(w, "tvdbid", &tvdb_id.to_string()); @@ -548,25 +845,26 @@ fn gen_common_nfo(w: &mut EventWriter<&mut Vec>, tmdb_tv: &TmdbTV) { }); // IMDB - tmdb_tv.imdb_id().is_some_then(|imdb_id| { + tmdb.imdb_id().is_some_then(|imdb_id| { // tt0898266 dom::write_text_element(w, "imdbid", imdb_id); // tt0898266 - w.write(XmlEvent::start_element("uniqueid").attr("type", "imdb").attr("default", "true")).unwrap(); + w.write(XmlEvent::start_element("uniqueid").attr("type", "imdb").attr("default", "true")) + .unwrap(); w.write(XmlEvent::characters(imdb_id)).unwrap(); w.write(XmlEvent::end_element()).unwrap(); }); // overview // plot - dom::write_cdata_text_element(w, "plot", &tmdb_tv.overview()); + dom::write_cdata_text_element(w, "plot", &tmdb.overview()); // outline - dom::write_cdata_text_element(w, "outline", &tmdb_tv.overview()); + dom::write_cdata_text_element(w, "outline", &tmdb.overview()); // director - tmdb_tv.directors().is_some_then(|directors| { + tmdb.directors().is_some_then(|directors| { directors.iter().for_each(|director| { w.write(XmlEvent::start_element("director").attr("tmdbid", &director.id())).unwrap(); w.write(XmlEvent::characters(director.name())).unwrap(); @@ -575,7 +873,7 @@ fn gen_common_nfo(w: &mut EventWriter<&mut Vec>, tmdb_tv: &TmdbTV) { }); // actors - tmdb_tv.actors().is_some_then(|actors| { + tmdb.actors().is_some_then(|actors| { actors.iter().for_each(|actor| { w.write(XmlEvent::start_element("actor")).unwrap(); @@ -591,10 +889,10 @@ fn gen_common_nfo(w: &mut EventWriter<&mut Vec>, tmdb_tv: &TmdbTV) { }); // genre - tmdb_tv.genres.is_some_then(|genres| { + tmdb.genres.is_some_then(|genres| { genres.iter().for_each(|genre| dom::write_text_element(w, "genre", genre.name())); }); // rating - dom::write_text_element(w, "rating", &tmdb_tv.vote_average()); + dom::write_text_element(w, "rating", &tmdb.vote_average()); } diff --git a/soda_resource_tools_lib/src/soda/tmdb/search.rs b/soda_resource_tools_lib/src/soda/tmdb/search.rs index 86e99baf..10df65b9 100644 --- a/soda_resource_tools_lib/src/soda/tmdb/search.rs +++ b/soda_resource_tools_lib/src/soda/tmdb/search.rs @@ -48,7 +48,14 @@ use crate::soda::{entity::SodaError, extension_option::OptionExtensions, tmdb::r // "total_results": 0 // } /// https://developer.themoviedb.org/reference/search-tv -pub(crate) fn search_tv(name: &str, year: Option<&str>, first_release_year: Option<&str>, include_adult: Option, page: usize) -> Result, SodaError> { +pub(crate) fn search_tv_with_language( + name: &str, + year: Option<&str>, + first_release_year: Option<&str>, + include_adult: Option, + page: usize, + language: &str, +) -> Result, SodaError> { let action = "/search/tv"; let mut params = format!("query={}&page={}", encode(name), page); @@ -60,10 +67,74 @@ pub(crate) fn search_tv(name: &str, year: Option<&str>, first_release_year: Opti include_adult.is_some_then(|value| params.push_str(&format!("&include_adult={}", value))); // 使用英文搜索更准确 - params.push_str("&language=en-US"); + params.push_str(&format!("&language={}", language)); match request::tmdb_request(action, ¶ms, "GET") { Ok(response) => Result::Ok(response.get("results").ok_or("get results error")?.as_array().unwrap().clone()), - Err(e) => Result::Err(SodaError::String(format!("search_tv error {:?}", e))), + Err(e) => { + tracing::error!("search_tv_with_language error {:?}", e); + Result::Err(SodaError::String(format!("search_tv_with_language error {:?}", e))) + } + } +} + +// { +// "page": 1, +// "results": [ +// { +// "adult": false, +// "backdrop_path": "/4HodYYKEIsGOdinkGi2Ucz6X9i0.jpg", +// "genre_ids": [ +// 16, +// 28, +// 12, +// 878 +// ], +// "id": 569094, +// "original_language": "en", +// "original_title": "Spider-Man: Across the Spider-Verse", +// "overview": "讲述了新生代蜘蛛侠迈尔斯(沙梅克·摩尔 Shameik Moore 配音)携手蜘蛛格温(海莉·斯坦菲尔德 Hailee Steinfeld 配音),穿越多元宇宙踏上更宏大的冒险征程的故事。面临每个蜘蛛侠都会失去至亲的宿命,迈尔斯誓言打破命运魔咒,找到属于自己的英雄之路。而这个决定和蜘蛛侠2099(奥斯卡·伊萨克 Oscar Is aac 配音)所领军的蜘蛛联盟产生了极大冲突,一场以一敌百的蜘蛛侠大内战即将拉响!", +// "popularity": 357.459, +// "poster_path": "/jBmJZDiJ2h6DimGROpxWlPh1xIo.jpg", +// "release_date": "2023-05-31", +// "title": "蜘蛛侠:纵横宇宙", +// "video": false, +// "vote_average": 8.376, +// "vote_count": 5548 +// } +// ], +// "total_pages": 1, +// "total_results": 1 +// } +// curl --request GET \ +// --url 'https://api.themoviedb.org/3/search/movie?query=Spider%20Man%20Across%20the%20Spider%20Verse&include_adult=false&language=zh-CN&page=1&year=2023' \ +// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2ZjViOTZkMGQ3MjUzMTE3YzQ0OTYzYTBjZThhYTZmMiIsInN1YiI6IjVjYmQzOTlmYzNhMzY4MTM2OTg1ZmM4ZSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.0ogVgNoOHpOswqSpl5Zg-_zxWEvAL2CO1TwIerbRbeo' \ +// --header 'accept: application/json' +pub(crate) fn search_movie_with_language(name: &str, year: Option<&str>, language: &str) -> Result, SodaError> { + let action = "/search/movie"; + + let mut params = format!("query={}&page={}", encode(name), 1); + + year.is_some_then(|value| params.push_str(&format!("&year={}", value))); + + // 使用英文搜索更准确 + params.push_str(&format!("&language={}", language)); + + match request::tmdb_request(action, ¶ms, "GET") { + Ok(response) => Result::Ok(response.get("results").ok_or("get results error")?.as_array().unwrap().clone()), + Err(e) => { + tracing::error!("search_movie_with_language error {:?}", e); + Result::Err(SodaError::String(format!("search_movie_with_language error {:?}", e))) + } + } +} + +#[cfg(test)] +mod search_test { + use super::search_movie_with_language; + + #[test] + fn test_search_movie_with_language() { + let value = search_movie_with_language("Spider Man Across the Spider Verse", Some("2023"), "en-US").unwrap(); } } diff --git a/soda_resource_tools_lib/src/soda/transfer.rs b/soda_resource_tools_lib/src/soda/transfer.rs index e840af1f..5ab53858 100644 --- a/soda_resource_tools_lib/src/soda/transfer.rs +++ b/soda_resource_tools_lib/src/soda/transfer.rs @@ -1,70 +1,112 @@ -use std::fs; use std::path::Path; +use std::{fs, path::PathBuf}; -use super::entity::{MTInfo, MTMetadata, TransferType}; +use crate::soda::entity::EmbyRenameStyle; -pub(crate) fn transfer(target_dir: &str, transfer_mode: &TransferType, rename_format: &str, file_metadata: &MTMetadata, src_path: &str) -> Option { +use super::entity::{MTInfo, MTMetadata, RenameStyle, SodaError, TransferType}; + +pub(crate) fn transfer( + target_dir: &str, + transfer_mode: &TransferType, + rename_style: Option, + rename_format: &str, + mt_meta: &MTMetadata, + src_path: &str, +) -> Result { // 生成转移文件路径 - if let Some(target_path) = get_mt_target_path(target_dir, rename_format, file_metadata) { - tracing::info!("target_path {:?}", target_path); - - // 转移文件 - let transfer_file = transfer_mt_file(file_metadata, src_path, &target_path, transfer_mode.clone()); - if let Some(transferred_path) = transfer_file { - tracing::info!("transfer file success"); - return Some(transferred_path); - } - return None; - } else { - tracing::info!("get target path error"); - return None; - } + let target_path = gen_mt_transfer_target_path(target_dir, rename_style, rename_format, mt_meta); + tracing::debug!("target_path {:?}", target_path); + + // 转移文件 + let transferred_path = transfer_mt_file(mt_meta, src_path, &target_path, transfer_mode)?; + return Ok(transferred_path); } /// 生成转移文件路径 -pub(crate) fn get_mt_target_path(target_dir: &str, rename_format: &str, metadata: &MTMetadata) -> Option { - if let Some(transfer_path) = gen_mt_rename_path(rename_format, metadata) { - let ret = Path::new(target_dir).join(transfer_path).to_string_lossy().to_string().replace("/", &std::path::MAIN_SEPARATOR.to_string()); - return Some(ret); +pub(crate) fn gen_mt_transfer_target_path(target_dir: &str, rename_style: Option, rename_format: &str, mt_meta: &MTMetadata) -> String { + tracing::debug!( + "gen_mt_transfer_target_path target_dir = {:?} rename_style = {:?}, rename_format = {:?}, mt_meta = {:?}", + target_dir, + rename_style, + rename_format, + mt_meta + ); + + if let Some(rename_style) = rename_style { + let transfer_path = gen_mt_rename_path2(rename_style, mt_meta); + + let path = Path::new(target_dir).join(transfer_path).to_string_lossy().to_string(); + + tracing::debug!("transfer_path after {:?}", path); + + return path; } else { - return None; + let transfer_path: String = gen_mt_rename_path(rename_format, mt_meta); + tracing::debug!("transfer_path before {:?}", transfer_path); + + let path = Path::new(target_dir) + .join(transfer_path) + .to_string_lossy() + .to_string() + .replace("/", &std::path::MAIN_SEPARATOR.to_string()); + + tracing::debug!("transfer_path after {:?}", path); + return path; } } -pub(crate) fn transfer_mt_file(mt_meta: &MTMetadata, src_file_path: &str, target_file_path: &str, transfer_mode: TransferType) -> Option { +/// 转移文件 +pub(crate) fn transfer_mt_file(mt_meta: &MTMetadata, src_path: &str, target_path: &str, transfer_type: &TransferType) -> Result { // check src_file_path is valid - if !Path::new(src_file_path).exists() { - tracing::info!("src_file_path {:?} is not exists", src_file_path); - return None; + if !Path::new(src_path).exists() { + return Err(SodaError::String(format!("src_file_path {:?} is not exists", src_path))); } - let target_path = Path::new(target_file_path); + let target_path = Path::new(target_path); + let target_path_str = target_path.to_str().unwrap().to_string(); // check transfer_path parent path is valid and create it if let Some(parent_path) = Path::new(&target_path).parent() { let parent_path = parent_path.to_str().unwrap().to_string(); - fs::create_dir_all(parent_path).unwrap_or_else(|err| { - tracing::error!("Failed to create directory: {}", err); - }); + fs::create_dir_all(parent_path)?; } // check target_path is exists and remove it if target_path.exists() { // fs::remove_file(target_path).unwrap(); - tracing::info!("target_path is exists, skip it"); - return Some(target_file_path.to_string()); + tracing::debug!("target_path is exists, skip it"); + return Ok(target_path_str); } // transfer file - return transfer_by_mode(transfer_mode, src_file_path, target_file_path); + return transfer_by_mode(transfer_type, src_path, &target_path_str); } /// 生成转移文件路径 -pub(crate) fn gen_mt_rename_path(rename_format: &str, mt_meta: &MTMetadata) -> Option { +fn gen_mt_rename_path2(rename_style: RenameStyle, mt_meta: &MTMetadata) -> PathBuf { + tracing::debug!("gen_mt_rename_path2 rename_style = {:?}, mt_meta = {:?}", rename_style, mt_meta); + + match rename_style { + RenameStyle::Emby => { + if mt_meta.is_movie() { + return EmbyRenameStyle::EmbyMovie.rename(mt_meta); + } else if mt_meta.is_tv() { + return EmbyRenameStyle::EmbyTV.rename(mt_meta); + } + } + } + unreachable!("gen_mt_rename_path2 = {:?}", rename_style); +} + +/// 生成转移文件路径 +pub(crate) fn gen_mt_rename_path(rename_format: &str, mt_meta: &MTMetadata) -> String { + tracing::debug!("gen_mt_rename_path rename_format = {:?}, mt_meta = {:?}", rename_format, mt_meta); + let mut result = rename_format.to_string(); // $title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$ if mt_meta.title_cn.is_empty() { + result = result.replace("$title_cn$.", ""); result = result.replace("$title_cn$", ""); result = result.replace("..", "."); } else { @@ -91,24 +133,21 @@ pub(crate) fn gen_mt_rename_path(rename_format: &str, mt_meta: &MTMetadata) -> O } } - match &mt_meta.year { - None => { - result = result.replace("$year$", ""); - result = result.replace("..", "."); - } - Some(year) => { - result = result.replace("$year$", year); - } + if mt_meta.year.is_empty() { + result = result.replace("$year$", ""); + result = result.replace("..", "."); + } else { + result = result.replace("$year$", &mt_meta.year); } if !mt_meta.season.is_empty() && !mt_meta.episode.is_empty() { - result = result.replace("$season$", &mt_meta.season); - result = result.replace("$episode$", &mt_meta.episode); + result = result.replace("$season$", &mt_meta.season_number_format()); + result = result.replace("$episode$", &mt_meta.episode_number_format()); } else if mt_meta.season.is_empty() && !mt_meta.episode.is_empty() { result = result.replace("$season$", ""); - result = result.replace("$episode$", &mt_meta.episode); + result = result.replace("$episode$", &mt_meta.episode_number_format()); } else if !mt_meta.season.is_empty() && mt_meta.episode.is_empty() { - result = result.replace("$season$", &mt_meta.season); + result = result.replace("$season$", &mt_meta.season_number_format()); result = result.replace("$episode$", ""); } else if mt_meta.season.is_empty() && mt_meta.episode.is_empty() { result = result.replace(".$season$$episode$.", "."); @@ -142,10 +181,12 @@ pub(crate) fn gen_mt_rename_path(rename_format: &str, mt_meta: &MTMetadata) -> O result = result.replace("$audio_codec$", &mt_meta.audio_codec); } - if mt_meta.extension.is_empty() { + if mt_meta.release_group.is_empty() { result = result.replace("$release_group$", ""); + result = result.replace(".-", ""); } else { result = result.replace("$release_group$", &mt_meta.release_group); + result = result.replace(".-", "-"); } if mt_meta.extension.is_empty() { @@ -154,66 +195,66 @@ pub(crate) fn gen_mt_rename_path(rename_format: &str, mt_meta: &MTMetadata) -> O result = result.replace("$extension$", &mt_meta.extension); } - return Some(result); + result = result.replace("./", "/"); + result = result.replace(".\\", "\\"); + + tracing::debug!("gen_mt_rename_path result = {:?}", result); + + return result; } -fn transfer_by_mode(mode: TransferType, src_file: &str, target_file: &str) -> Option { +fn transfer_by_mode(mode: &TransferType, src_file: &str, target_file: &str) -> Result { + tracing::debug!( + "transfer_by_mode mode = {:?},src_file_exist = {}, src_file = {:?}, target_file = {:?}", + mode, + Path::new(src_file).exists(), + src_file, + target_file + ); match mode { TransferType::HardLink => { return match fs::hard_link(src_file, target_file) { - Ok(()) => Some(target_file.to_string()), - Err(e) => { - tracing::info!("Failed to create hard link: {}", e); - None - } + Ok(()) => Ok(target_file.to_string()), + Err(e) => Err(SodaError::String(format!("Failed to create hard link: {}", e))), }; } TransferType::SymbolLink => { return match create_symlink(src_file, target_file) { - Ok(()) => Some(target_file.to_string()), - Err(e) => { - tracing::info!("Failed to create soft link: {}", e); - None - } + Ok(()) => Ok(target_file.to_string()), + Err(e) => Err(SodaError::String(format!("Failed to create soft link: {}", e))), }; } TransferType::Copy => { return match fs::copy(src_file, target_file) { - Ok(_) => Some(target_file.to_string()), - Err(e) => { - tracing::info!("Failed to move file: {}", e); - None - } + Ok(_) => Ok(target_file.to_string()), + Err(e) => Err(SodaError::String(format!("Failed to move file: {}", e))), }; } TransferType::Move => { return match fs::copy(src_file, target_file) { Ok(_) => { fs::remove_file(src_file).unwrap_or_else(|e| { - tracing::info!("Failed to remove file: {}", e); + tracing::error!("Failed to remove file: {}", e); }); - Some(target_file.to_string()) - } - Err(e) => { - tracing::info!("Failed to move file: {}", e); - None + Ok(target_file.to_string()) } + Err(e) => Err(SodaError::String(format!("Failed to move file: {}", e))), }; } } } -fn create_symlink(target_path: &str, symlink_path: &str) -> Result<(), std::io::Error> { +fn create_symlink(src_path: &str, target_path: &str) -> Result<(), std::io::Error> { // 使用条件编译来根据平台选择不同的符号链接函数 #[cfg(target_os = "windows")] { // 在 Windows 上使用 std::os::windows::fs::symlink_file - std::os::windows::fs::symlink_file(target_path, symlink_path)?; + std::os::windows::fs::symlink_file(src_path, target_path)?; } #[cfg(not(target_os = "windows"))] { // 在 Unix-like 系统上使用 std::os::unix::fs::symlink - std::os::unix::fs::symlink(target_path, symlink_path)?; + std::os::unix::fs::symlink(src_path, target_path)?; } Ok(()) @@ -221,42 +262,109 @@ fn create_symlink(target_path: &str, symlink_path: &str) -> Result<(), std::io:: #[cfg(test)] mod transfer_tests { - use crate::soda::meta::{self}; + use tracing::level_filters::LevelFilter; + use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter}; + + use crate::soda::{ + entity::MetaContext, + meta::{self}, + }; + + use crate::soda::fs::metadata; use super::*; + static mut IS_TRACING_INIT: bool = false; + + /// 初始化日志配置 + fn init_tracing() { + unsafe { + if !IS_TRACING_INIT { + IS_TRACING_INIT = true; + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::DEBUG.into())) + .with_span_events(FmtSpan::FULL) + .init(); + } else { + } + } + } + #[test] fn test_generate_transfer_path() { - let metadata = meta::mt_metadata("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4").unwrap(); - let ret = gen_mt_rename_path("$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", &metadata); - assert_eq!(ret.unwrap(), "凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC.mp4") + init_tracing(); + + let mut scrape_context = MetaContext::new(); + scrape_context.init("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4"); + let metadata = meta::create_metadata_mt(&mut scrape_context).unwrap(); + let ret = gen_mt_rename_path( + "$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", + &metadata, + ); + assert_eq!(ret, "凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC.mp4") } #[test] fn test_generate_transfer_path1() { - let metadata = meta::mt_metadata("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4").unwrap(); + init_tracing(); + + let mut scrape_context = MetaContext::new(); + scrape_context.init("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4"); + let metadata = meta::create_metadata_mt(&mut scrape_context).unwrap(); let ret = gen_mt_rename_path("$title_cn$.$title_en$.$year$/$title_cn$.$title_en$.$year$.$season$.$resolution$.$source$.$video_codec$.$audio_codec$/$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", &metadata); - assert_eq!(ret.unwrap(), "凡人修仙传.The.Mortal.Ascention.2020/凡人修仙传.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H.264.AAC/凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC.mp4") + assert_eq!(ret, "凡人修仙传.The.Mortal.Ascention.2020/凡人修仙传.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H.264.AAC/凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC.mp4") } #[test] fn test_generate_transfer_path2() { - let metadata = meta::mt_metadata("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.mp4").unwrap(); + init_tracing(); + + let mut scrape_context = MetaContext::new(); + scrape_context.init("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.mp4"); + let metadata = meta::create_metadata_mt(&mut scrape_context).unwrap(); let ret = gen_mt_rename_path("$title_cn$.$title_en$.$year$/$title_cn$.$title_en$.$year$.$season$.$resolution$.$source$.$video_codec$.$audio_codec$/$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", &metadata); - assert_eq!(ret.unwrap(), "凡人修仙传.The.Mortal.Ascention.2020/凡人修仙传.The.Mortal.Ascention.2020.S01.2160p/凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.mp4") + assert_eq!(ret, "凡人修仙传.The.Mortal.Ascention.2020/凡人修仙传.The.Mortal.Ascention.2020.S01.2160p/凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.mp4") } #[test] fn test_generate_transfer_path3() { - let metadata = meta::mt_metadata("Friends.S01E02.1080p.BluRay.Remux.AVC.AC3-WhaleHu.mkv").unwrap(); - let ret = gen_mt_rename_path("$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", &metadata); - assert_eq!(ret.unwrap(), "Friends.S01E02.1080p.BluRay.Remux.H.264.AC3.mkv") + init_tracing(); + + let mut scrape_context = MetaContext::new(); + scrape_context.init("Friends.S01E02.1080p.BluRay.Remux.AVC.AC3-WhaleHu.mkv"); + let metadata = meta::create_metadata_mt(&mut scrape_context).unwrap(); + let ret = gen_mt_rename_path( + "$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", + &metadata, + ); + assert_eq!(ret, "Friends.S01E02.1080p.BluRay.Remux.H.264.AC3.mkv") } #[test] fn test_generate_transfer_path4() { - let metadata = meta::mt_metadata("The.Long.Season.E01.2023.1080p.WEBrip.NF.x265.10bit.AC3£cXcY@FRDS.mkv").unwrap(); - let ret = gen_mt_rename_path("$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", &metadata); - assert_eq!(ret.unwrap(), "The.Long.Season.2023.E01.1080p.WEBrip.H.265.AC3.mkv") + init_tracing(); + + let mut scrape_context = MetaContext::new(); + scrape_context.init("The.Long.Season.E01.2023.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv"); + let metadata = meta::create_metadata_mt(&mut scrape_context).unwrap(); + let ret = gen_mt_rename_path( + "$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", + &metadata, + ); + assert_eq!(ret, "The.Long.Season.2023.E01.1080p.WEBrip.H.265.AC3.mkv") + } + + #[test] + fn test_generate_transfer_path5() { + init_tracing(); + + let mut scrape_context = MetaContext::new(); + scrape_context.init("大侠霍元甲.Fearless.2020.E01.2160P.WEB-DL.H265.AAC-HDHWEB.mp4"); + let metadata = meta::create_metadata_mt(&mut scrape_context).unwrap(); + let ret = gen_mt_rename_path( + "$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$", + &metadata, + ); + assert_eq!(ret, "大侠霍元甲.Fearless.2020.E01.2160p.WEB-DL.H.265.AAC.mp4") } } diff --git a/soda_resource_tools_lib/src/soda/utils.rs b/soda_resource_tools_lib/src/soda/utils.rs index 5371f7f4..1c64e4c8 100644 --- a/soda_resource_tools_lib/src/soda/utils.rs +++ b/soda_resource_tools_lib/src/soda/utils.rs @@ -9,6 +9,7 @@ pub mod token; use std::fs::OpenOptions; use std::io::Write; +use super::entity::SodaError; use super::LIB_CONFIG; pub fn is_bluray_dir(path: &str) -> bool { @@ -89,9 +90,14 @@ pub(crate) fn get_tmdb_http_cache_path() -> String { http_cache } - pub(crate) fn get_fanart_http_cache_path() -> String { let cache_path = get_cache_path(); let http_cache = cache_path.join("fanart_http_cache").to_str().unwrap().to_string(); http_cache } + +pub(crate) fn get_path_file_name(path: &str) -> Result { + let path = Path::new(path); + let file_name = path.file_name().and_then(|s| s.to_str()).ok_or(SodaError::Str("file_name is empty"))?; + Ok(file_name.to_string()) +} diff --git a/soda_resource_tools_lib/src/soda/utils/encrypt.rs b/soda_resource_tools_lib/src/soda/utils/encrypt.rs index 32ae9dcf..b77bf593 100644 --- a/soda_resource_tools_lib/src/soda/utils/encrypt.rs +++ b/soda_resource_tools_lib/src/soda/utils/encrypt.rs @@ -52,7 +52,13 @@ pub fn verify_password(password: &str, actual_password: &str, salt: &str) -> Res let n_iter: NonZeroU32 = NonZeroU32::new(100_000).unwrap(); // 较验密码是否匹配 - match pbkdf2::verify(PBKDF2_ALG, n_iter, salt.as_bytes(), password.as_bytes(), actual_password_decode.as_slice()) { + match pbkdf2::verify( + PBKDF2_ALG, + n_iter, + salt.as_bytes(), + password.as_bytes(), + actual_password_decode.as_slice(), + ) { Ok(_) => Ok(true), _ => Err("Failed".to_string()), } @@ -60,6 +66,7 @@ pub fn verify_password(password: &str, actual_password: &str, salt: &str) -> Res #[cfg(test)] mod test { + use super::{encrypt_password, generate_salt, verify_password}; /// 生成64位随机salt diff --git a/soda_resource_tools_lib/src/soda/utils/token.rs b/soda_resource_tools_lib/src/soda/utils/token.rs index 8712ef55..bd8605fd 100644 --- a/soda_resource_tools_lib/src/soda/utils/token.rs +++ b/soda_resource_tools_lib/src/soda/utils/token.rs @@ -44,14 +44,33 @@ mod test { #[test] fn test_create_token() { - let token = create_token("soda", Token { key: "time".to_string(), expire_time_millis: 60 * 24 * 7 * 1000 }); + let token = create_token( + "soda", + Token { + key: "time".to_string(), + expire_time_millis: 60 * 24 * 7 * 1000, + }, + ); println!("access_token = {}", token); - assert_eq!(token, "eyJhbGciOiJIUzI1NiJ9.eyJleHBpcmVfdGltZV9taWxsaXMiOiIxMDA4MDAwMCIsImtleSI6InRpbWUifQ.qK5Ff-aqKPoo15R3afHo5C4dV0XJhMEhsAwrR-UANwM"); + assert_eq!( + token, + "eyJhbGciOiJIUzI1NiJ9.eyJleHBpcmVfdGltZV9taWxsaXMiOiIxMDA4MDAwMCIsImtleSI6InRpbWUifQ.qK5Ff-aqKPoo15R3afHo5C4dV0XJhMEhsAwrR-UANwM" + ); } #[test] fn test_verification_token() { - let token = verification_token("soda", "eyJhbGciOiJIUzI1NiJ9.eyJleHBpcmVfdGltZV9taWxsaXMiOiIxMDA4MDAwMCIsImtleSI6InRpbWUifQ.qK5Ff-aqKPoo15R3afHo5C4dV0XJhMEhsAwrR-UANwM").unwrap(); - assert_eq!(token, Token { key: "time".to_string(), expire_time_millis: 60 * 24 * 7 * 1000 }); + let token = verification_token( + "soda", + "eyJhbGciOiJIUzI1NiJ9.eyJleHBpcmVfdGltZV9taWxsaXMiOiIxMDA4MDAwMCIsImtleSI6InRpbWUifQ.qK5Ff-aqKPoo15R3afHo5C4dV0XJhMEhsAwrR-UANwM", + ) + .unwrap(); + assert_eq!( + token, + Token { + key: "time".to_string(), + expire_time_millis: 60 * 24 * 7 * 1000 + } + ); } } diff --git a/soda_resource_tools_lib/src/soda/watcher.rs b/soda_resource_tools_lib/src/soda/watcher.rs index 708747ea..202996df 100644 --- a/soda_resource_tools_lib/src/soda/watcher.rs +++ b/soda_resource_tools_lib/src/soda/watcher.rs @@ -40,7 +40,7 @@ where handler.handle_other_event(event.paths); } }, - Err(error) => tracing::info!("Error: {error:?}"), + Err(error) => tracing::debug!("Error: {error:?}"), } } } diff --git a/soda_resource_tools_lib/tests/config_test.rs b/soda_resource_tools_lib/tests/config_test.rs new file mode 100644 index 00000000..66aa02db --- /dev/null +++ b/soda_resource_tools_lib/tests/config_test.rs @@ -0,0 +1,119 @@ +#[cfg(test)] +mod config_tests { + use std::{ + fs::File, + io::{Read, Write}, + }; + + use magic_crypt::{new_magic_crypt, MagicCryptTrait}; + use serde_json::{json, Value}; + + // #[test] + fn gen_bin() { + let current_path = std::env::current_dir().unwrap(); + let strong_match_rules_tv_path = current_path + .join("config") + .join("mt_strong_match_rules_tv.json") + .to_str() + .unwrap() + .to_string(); + let strong_match_rules_movie_path = current_path + .join("config") + .join("mt_strong_match_rules_movie.json") + .to_str() + .unwrap() + .to_string(); + let strong_match_regex_rules_path = current_path + .join("config") + .join("mt_strong_match_regex_rules.json") + .to_str() + .unwrap() + .to_string(); + let strong_match_name_map_path = current_path + .join("config") + .join("mt_strong_match_name_map.json") + .to_str() + .unwrap() + .to_string(); + + // 合并 JSON 数据 + let mut combined_json = json!({}); + + // tv + { + let mut file = File::open(strong_match_rules_tv_path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + let mut value: Value = serde_json::from_str(&contents).unwrap(); + let rules = value.get_mut("rules").unwrap().as_array_mut().unwrap(); + for ele in rules { + ele["example"] = Value::String("".to_string()); + } + combined_json["mt_strong_match_rules_tv"] = value; + } + + // movie + { + let mut file = File::open(strong_match_rules_movie_path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + let mut value: Value = serde_json::from_str(&contents).unwrap(); + let rules = value.get_mut("rules").unwrap().as_array_mut().unwrap(); + for ele in rules { + ele["example"] = Value::String("".to_string()); + } + combined_json["mt_strong_match_rules_movie"] = value; + } + + // rules + { + let mut file = File::open(strong_match_regex_rules_path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + let value: Value = serde_json::from_str(&contents).unwrap(); + combined_json["mt_strong_match_regex_rules"] = value; + } + + // map + { + let mut file = File::open(strong_match_name_map_path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + let mut value: Value = serde_json::from_str(&contents).unwrap(); + let names = value.get_mut("names").unwrap().as_array_mut().unwrap(); + for ele in names { + ele["example"] = Value::String("".to_string()); + } + combined_json["mt_strong_match_name_map"] = value; + } + + let mut bin_out_file = File::create( + current_path + .parent() + .unwrap() + .parent() + .unwrap() + .join("soda_cl") + .join("soda_config_rule.json"), + ) + .unwrap(); + + let combined_str = serde_json::to_string(&combined_json).unwrap(); + + // let mc = new_magic_crypt!("biezhihua_soda", 256); + + // let encoded = mc.encrypt_str_to_base64(combined_str); + + bin_out_file.write_all(combined_str.as_bytes()).unwrap(); + + let soda_config_json = json!({ + "version": 0, + "bin": "soda_config_rule.json", + "enable_cli": true, + }); + + let mut json_out_file = File::create(current_path.parent().unwrap().parent().unwrap().join("soda_cl").join("soda_config.json")).unwrap(); + let combined_str = serde_json::to_string(&soda_config_json).unwrap(); + json_out_file.write_all(combined_str.as_bytes()).unwrap(); + } +} diff --git a/soda_resource_tools_lib/tests/integration_1_soda_test.rs b/soda_resource_tools_lib/tests/integration_1_soda_test.rs new file mode 100644 index 00000000..d8979115 --- /dev/null +++ b/soda_resource_tools_lib/tests/integration_1_soda_test.rs @@ -0,0 +1,547 @@ +#[cfg(test)] +mod soda_tests { + use std::path::PathBuf; + + use soda_resource_tools_lib::soda; + use soda_resource_tools_lib::soda::entity::ScrapeConfig; + use tracing::level_filters::LevelFilter; + use tracing_subscriber::fmt::format::FmtSpan; + use tracing_subscriber::EnvFilter; + + fn tests_dir() -> std::path::PathBuf { + std::env::current_dir().unwrap().join("tests") + } + + #[test] + fn test_scrape_tv_1() { + test_scrape_tv( + "test_scrape_tv_1", + "Moving.S01E01.2023.1080p.WEB-DL.x265.10bit.AC3£cXcY@FRDS.mkv", + "超异能族.Moving.2023", + "超异能族.Moving.2023.S01.1080p.WEB-DL.H.265.AC3-cXcY@FRDS", + "超异能族.Moving.2023.S01E01.1080p.WEB-DL.H.265.AC3-cXcY@FRDS.mkv", + ) + } + + #[test] + fn test_scrape_tv_2() { + // 测试 - 仅在第一季按照发布年份查找 + test_scrape_tv( + "test_scrape_tv_2", + "The.It.Crowd.S02E01.2007.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "IT狂人.The.IT.Crowd.2006", + "IT狂人.The.IT.Crowd.2007.S02.1080p.WEBrip.H.265.AC3-cXcY@FRDS", + "IT狂人.The.IT.Crowd.2007.S02E01.1080p.WEBrip.H.265.AC3-cXcY@FRDS.mkv", + ) + } + + #[test] + fn test_scrape_tv_3() { + // 测试 - 移除名字中的特殊字符 - , + test_scrape_tv( + "test_scrape_tv_3", + "Yes,Minister.S01E07.1980.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "是,大臣.Yes,Minister.1980", + "是,大臣.Yes,Minister.1980.S01.1080p.WEBrip.H.265.AC3-cXcY@FRDS", + "是,大臣.Yes,Minister.1980.S01E07.1080p.WEBrip.H.265.AC3-cXcY@FRDS.mkv", + ) + } + + #[test] + fn test_scrape_tv_4() { + // 测试 - 特殊符号移除 + test_scrape_tv( + "test_scrape_tv_4", + "[壹高清]你喜欢勃拉姆斯吗.Do You Like Brahms.Ep01.HDTV.720p.H264-OneHD.mkv", + "你喜欢勃拉姆斯吗.Do.You.Like.Brahms.2020", + "你喜欢勃拉姆斯吗.Do.You.Like.Brahms.2020.S01.720p.HDTV.H.264-OneHD", + "你喜欢勃拉姆斯吗.Do.You.Like.Brahms.2020.S01E01.720p.HDTV.H.264-OneHD.mkv", + ) + } + + #[test] + fn test_scrape_tv_5() { + // 测试 - 特殊符号移除 + test_scrape_tv( + "test_scrape_tv_5", + "The.Witcher.Blood.Origin.S01E01.2022.Netflix.WEB-DL.1080p.x264.DDP-HDCTV.mkv", + "猎魔人:血源.The.Witcher.Blood.Origin.2022", + "猎魔人:血源.The.Witcher.Blood.Origin.2022.S01.1080p.WEB-DL.H.264.DDP-HDCTV", + "猎魔人:血源.The.Witcher.Blood.Origin.2022.S01E01.1080p.WEB-DL.H.264.DDP-HDCTV.mkv", + ) + } + + #[test] + fn test_scrape_tv_6() { + // 测试 - 特殊符号移除 + test_scrape_tv( + "test_scrape_tv_6", + "SAS.Rogue.Heroes.S01E02.2022.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "SAS:叛逆勇士.SAS.Rogue.Heroes.2022", + "SAS:叛逆勇士.SAS.Rogue.Heroes.2022.S01.1080p.BluRay.H.265.AC3-cXcY@FRDS", + "SAS:叛逆勇士.SAS.Rogue.Heroes.2022.S01E02.1080p.BluRay.H.265.AC3-cXcY@FRDS.mkv", + ) + } + + #[test] + fn test_scrape_tv_7() { + // 测试 - 第一季的年份不准确导致查询失败 + // 测试 - 第一季的年份不准确则查询成功后合并季年份 + test_scrape_tv( + "test_scrape_tv_7", + "Sneaky.Pete.S01E01.2017.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "诈欺担保人.Sneaky.Pete.2015", + "诈欺担保人.Sneaky.Pete.2015.S01.1080p.BluRay.H.265.AC3-cXcY@FRDS", + "诈欺担保人.Sneaky.Pete.2015.S01E01.1080p.BluRay.H.265.AC3-cXcY@FRDS.mkv", + ) + } + + #[test] + fn test_scrape_tv_8() { + test_scrape_tv( + "test_scrape_tv_8", + "Friends.S01E01.1080p.BluRay.Remux.AVC.AC3-WhaleHu.mkv", + "老友记.Friends.1994", + "老友记.Friends.1994.S01.1080p.BluRay.Remux.H.264.AC3-WhaleHu", + "老友记.Friends.1994.S01E01.1080p.BluRay.Remux.H.264.AC3-WhaleHu.mkv", + ) + } + + #[test] + fn test_scrape_tv_9() { + test_scrape_tv( + "test_scrape_tv_9", + "Friends.S02E01.1080p.BluRay.Remux.AVC.AC3-WhaleHu.mkv", + "老友记.Friends.1994", + "老友记.Friends.1995.S02.1080p.BluRay.Remux.H.264.AC3-WhaleHu", + "老友记.Friends.1995.S02E01.1080p.BluRay.Remux.H.264.AC3-WhaleHu.mkv", + ) + } + + #[test] + fn test_scrape_tv_10() { + test_scrape_tv( + "test_scrape_tv_10", + "JSTV.The.Guardian.E01.2022.HDTV.1080i.H264-HDCTV.mkv", + "护卫者.The.Guardian.2022", + "护卫者.The.Guardian.2022.S01.1080i.HDTV.H.264-HDCTV", + "护卫者.The.Guardian.2022.S01E01.1080i.HDTV.H.264-HDCTV.mkv", + ) + } + + #[test] + fn test_scrape_tv_11() { + test_scrape_tv( + "test_scrape_tv_11", + "大侠霍元甲.Fearless.2020.E01.2160P.WEB-DL.H265.AAC-HDHWEB.mp4", + "大侠霍元甲.Fearless.2020", + "大侠霍元甲.Fearless.2020.S01.2160p.WEB-DL.H.265.AAC-HDHWEB", + "大侠霍元甲.Fearless.2020.S01E01.2160p.WEB-DL.H.265.AAC-HDHWEB.mp4", + ) + } + + #[test] + fn test_scrape_tv_12() { + test_scrape_tv( + "test_scrape_tv_12", + "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.S01E01.Standing.Up.in.the.Milky.Way.2014.BluRay.1080p.x265.10Bit.DTS.5.1.内封中字简繁双语特效-FFans@ws林小凡.mkv", + "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.2014", + "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.2014.S01.1080p.BluRay.H.265.DTS.5.1-FFans@ws林小凡", + "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.2014.S01E01.1080p.BluRay.H.265.DTS.5.1-FFans@ws林小凡.mkv" + ) + } + + #[test] + fn test_scrape_tv_13() { + test_scrape_tv( + "test_scrape_tv_13", + "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.S01E01.Standing.Up.in.the.Milky.Way.2014.BluRay.1080p.x265.10Bit.DTS.5.1.内封中字简繁双语特效-FFans@ws林小凡.mkv", + "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.2014", + "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.2014.S01.1080p.BluRay.H.265.DTS.5.1-FFans@ws林小凡", + "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.2014.S01E01.1080p.BluRay.H.265.DTS.5.1-FFans@ws林小凡.mkv" + ) + } + + #[test] + fn test_scrape_tv_14() { + test_scrape_tv( + "test_scrape_tv_14", + "City.of.Angels.City.of.Death.S01E02.2021.Disney+.WEB-DL.1080p.H264.DDP-HDCTV.mkv", + "City.of.Angels.City.of.Death.2021", + "City.of.Angels.City.of.Death.2021.S01.1080p.WEB-DL.H.264.DDP-HDCTV", + "City.of.Angels.City.of.Death.2021.S01E02.1080p.WEB-DL.H.264.DDP-HDCTV.mkv", + ) + } + + #[test] + fn test_scrape_tv_15() { + test_scrape_tv( + "test_scrape_tv_15", + "Skam.S1E01.你看起来像个荡女彐.中挪字幕.WEBRip.1080P.不着调字幕组.mkv", + "羞耻.SKAM.2015", + "羞耻.SKAM.2015.S01.1080p.WEBRip-不着调字幕组", + "羞耻.SKAM.2015.S01E01.1080p.WEBRip-不着调字幕组.mkv", + ); + + test_scrape_tv( + "test_scrape_tv_15", + "Skam.S2E02.你对一个朋友撒谎却怪罪于我.SweSub.1080p.WEB-DL.H264.mp4", + "羞耻.SKAM.2015", + "羞耻.SKAM.2016.S02.1080p.WEB-DL.H.264", + "羞耻.SKAM.2016.S02E02.1080p.WEB-DL.H.264.mp4", + ); + } + + #[test] + fn test_scrape_tv_16() { + // 电视剧多季解析成了不同的电视剧 + test_scrape_tv( + "test_scrape_tv_16", + "Suits.S01E01.2018.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "金装律师(JP).Suits.2018", + "金装律师(JP).Suits.2018.S01.1080p.WEBrip.H.265.AC3-cXcY@FRDS", + "金装律师(JP).Suits.2018.S01E01.1080p.WEBrip.H.265.AC3-cXcY@FRDS.mkv", + ); + + // 电视剧多季解析成了不同的电视剧 + // test_scrape_tv( + // "test_scrape_tv_16", + // "Suits.S02E01.2020.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + // "金装律师(JP).Suits.2018", + // "金装律师(JP).Suits.2020.S02.1080p.WEBrip.H.265.AC3-cXcY@FRDS", + // "金装律师(JP).Suits.2020.S02E01.1080p.WEBrip.H.265.AC3-cXcY@FRDS.mkv", + // ) + } + + #[test] + fn test_scrape_tv_17() { + test_scrape_tv( + "test_scrape_tv_17", + "AOD.百万同居计划.Million Dollar Family.Ep01.HDTV.1080p.H264-OneHD.mkv", + "百万同居计划.Million.Dollar.Family.2022", + "百万同居计划.Million.Dollar.Family.2022.S01.1080p.HDTV.H.264-OneHD", + "百万同居计划.Million.Dollar.Family.2022.S01E01.1080p.HDTV.H.264-OneHD.mkv", + ); + } + + #[test] + fn test_scrape_tv_18() { + test_scrape_tv( + "test_scrape_tv_18", + "A.Perfect.Planet.S01E01.BluRay.2160p.Atmos.TrueHD.7.1.HDR.x265.10bit-CHD.mkv", + "完美星球.A.Perfect.Planet.2021", + "完美星球.A.Perfect.Planet.2021.S01.2160p.BluRay.H.265.Atmos.TrueHD.7.1-CHD", + "完美星球.A.Perfect.Planet.2021.S01E01.2160p.BluRay.H.265.Atmos.TrueHD.7.1-CHD.mkv", + ); + } + + #[test] + fn test_scrape_tv_19() { + test_scrape_tv( + "test_scrape_tv_19", + "Taiwan.Taste.S01E07.2012.1080p.KKTV.WEB-DL.H264.AAC-HHWEB.mkv", + "Taiwan.Taste", + "Taiwan.Taste.2012.S01.1080p.WEB-DL.H.264.AAC-HHWEB", + "Taiwan.Taste.2012.S01E07.1080p.WEB-DL.H.264.AAC-HHWEB.mkv", + ); + } + + #[test] + fn test_scrape_tv_20() { + let target = PathBuf::new() + .join("Lost.S01-S06.COMPLETE.2004-2010.1080p.Blu-ray.x265.10bits.DTS.5.1-PTer") + .join("S01-PTer") + .join("S01E02-PTer.mkv"); + test_scrape_tv2( + "test_scrape_tv_20", + &target, + "迷失.Lost.2004", + "迷失.Lost.2004.S01-PTer", + "迷失.Lost.2004.S01E02-PTer.mkv", + ); + } + + #[test] + fn test_scrape_tv_21() { + test_scrape_tv( + "test_scrape_tv_21", + "Yan.Huo.Shi.Wei.2021.E01.1080p.WEB-DL.H264.AAC-LeagueWEB.mp4", + "烟火拾味.Yan.Huo.Shi.Wei.2021", + "烟火拾味.Yan.Huo.Shi.Wei.2021.S01.1080p.WEB-DL.H.264.AAC-LeagueWEB", + "烟火拾味.Yan.Huo.Shi.Wei.2021.S01E01.1080p.WEB-DL.H.264.AAC-LeagueWEB.mp4", + ); + } + + #[test] + fn test_scrape_tv_22() { + test_scrape_tv( + "test_scrape_tv_22", + "The.College.Entrance.Exam.2015.E01.WEB-DL.1080p.H264.AAC-PTerWEB.mp4", + "高考.The.College.Entrance.Exam.2015", + "高考.The.College.Entrance.Exam.2015.S01.1080p.WEB-DL.H.264.AAC-PTerWEB", + "高考.The.College.Entrance.Exam.2015.S01E01.1080p.WEB-DL.H.264.AAC-PTerWEB.mp4", + ); + } + + #[test] + fn test_scrape_tv_23() { + test_scrape_tv( + "test_scrape_tv_23", + "[易中天品三国01大江东去].Yi.Zhong.Tian.Pin.San.Guo.E01.2006.DVDRip.576p.x264.AC3-CMCT.mkv", + "易中天品三国.Yi.Zhong.Tian.Pin.San.Guo.2006", + "易中天品三国.Yi.Zhong.Tian.Pin.San.Guo.2006.S01.576p.DVDRip.H.264.AC3-CMCT", + "易中天品三国.Yi.Zhong.Tian.Pin.San.Guo.2006.S01E01.576p.DVDRip.H.264.AC3-CMCT.mkv", + ); + } + + #[test] + fn test_scrape_tv_24() { + test_scrape_tv( + "test_scrape_tv_24", + "The.Greed.of.Man.E01.1992.1080p.WEBrip.x265.10bit.AC3£cXcY@FRDS.mkv", + "大时代.The.Greed.of.Man.1992", + "大时代.The.Greed.of.Man.1992.S01.1080p.WEBrip.H.265.AC3-cXcY@FRDS", + "大时代.The.Greed.of.Man.1992.S01E01.1080p.WEBrip.H.265.AC3-cXcY@FRDS.mkv", + ); + } + + #[test] + fn test_scrape_tv_25() { + test_scrape_tv( + "test_scrape_tv_25", + "The.Ivory.Tower.S01E01.2003.2160p.WEB-DL.H264.60fps.AAC-HHWEB.mp4", + "白色巨塔.The.Great.White.Tower.2003", + "白色巨塔.The.Great.White.Tower.2003.S01.2160p.WEB-DL.H.264.AAC-HHWEB", + "白色巨塔.The.Great.White.Tower.2003.S01E01.2160p.WEB-DL.H.264.AAC-HHWEB.mp4", + ) + } + + #[test] + fn test_scrape_tv_26() { + test_scrape_tv( + "test_scrape_tv_26", + "24小时.S01E01.2001.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "24.2001", + "24.2001.S01.1080p.BluRay.H.265.AC3-cXcY@FRDS", + "24.2001.S01E01.1080p.BluRay.H.265.AC3-cXcY@FRDS.mkv", + ) + } + + #[test] + fn test_scrape_tv_27() { + test_scrape_tv( + "test_scrape_tv_27", + "与摩根·弗里曼一起穿越虫洞.Through.The.Wormhole.With.Morgan.Freeman.2010.S01E05.4K.WEB-DL.H265.AAC-PTerWEB.mp4", + "与摩根·弗里曼一起穿越虫洞.Through.the.Wormhole.2010", + "与摩根·弗里曼一起穿越虫洞.Through.the.Wormhole.2010.S01.2160p.WEB-DL.H.265.AAC-PTerWEB", + "与摩根·弗里曼一起穿越虫洞.Through.the.Wormhole.2010.S01E05.2160p.WEB-DL.H.265.AAC-PTerWEB.mp4", + ) + } + + #[test] + fn test_scrape_tv_28() { + test_scrape_tv( + "test_scrape_tv_28", + "History.of.the.World.2008.S01E02.2160p.WEB-DL.H265.AAC-LeagueWEB.mp4", + "世界历史.History.of.the.World.2008", + "世界历史.History.of.the.World.2008.S01.2160p.WEB-DL.H.265.AAC-LeagueWEB", + "世界历史.History.of.the.World.2008.S01E02.2160p.WEB-DL.H.265.AAC-LeagueWEB.mp4", + ) + } + + #[test] + fn test_scrape_movie_1() { + test_scrape_movie( + "test_scrape_movie_1", + "Spider-Man.Across.the.Spider-Verse.2023.2160p.MA.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX.mkv", + "蜘蛛侠:纵横宇宙.Spider.Man.Across.the.Spider.Verse.2023.2160p.WEB-DL.H.265.DDP.5.1.Atmos", + "蜘蛛侠:纵横宇宙.Spider.Man.Across.the.Spider.Verse.2023.2160p.WEB-DL.H.265.DDP.5.1.Atmos.mkv", + ); + } + + #[test] + fn test_scrape_movie_2() { + test_scrape_movie( + "test_scrape_movie_2", + "To.the.Wonder.2012.BluRay.1080p.DTS-HDMA.7.1.AVC.REMUX-FraMeSToR-4P.mkv", + "通往仙境.To.the.Wonder.2012.1080p.BluRay.H.264.DTS-HDMA.7.1", + "通往仙境.To.the.Wonder.2012.1080p.BluRay.H.264.DTS-HDMA.7.1.mkv", + ); + } + + #[test] + fn test_scrape_movie_3() { + test_scrape_movie( + "test_scrape_movie_3", + "A.Chinese.Odyssey.Part.2.1995.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "大话西游之仙履奇缘.A.Chinese.Odyssey.Part.Two.Cinderella.1995.1080p.BluRay.H.265.2Audio", + "大话西游之仙履奇缘.A.Chinese.Odyssey.Part.Two.Cinderella.1995.1080p.BluRay.H.265.2Audio.mkv", + ); + } + + #[test] + fn test_scrape_movie_4() { + test_scrape_movie( + "test_scrape_movie_4", + "A.Chinese.Odyssey.Part.1.1995.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "大话西游之月光宝盒.A.Chinese.Odyssey.Part.One.Pandoras.Box.1995.1080p.BluRay.H.265.2Audio", + "大话西游之月光宝盒.A.Chinese.Odyssey.Part.One.Pandoras.Box.1995.1080p.BluRay.H.265.2Audio.mkv", + ); + } + + #[test] + fn test_scrape_movie_5() { + test_scrape_movie( + "test_scrape_movie_5", + "Parasite.AKA.Gisaengchung.2019.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "寄生虫.Parasite.2019.1080p.BluRay.H.265", + "寄生虫.Parasite.2019.1080p.BluRay.H.265.mkv", + ); + } + + #[test] + fn test_scrape_movie_6() { + test_scrape_movie( + "test_scrape_movie_6", + "Hachi.A.Dog'sTale.2009.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "忠犬八公的故事.Hachi.A.Dog's.Tale.2009.1080p.BluRay.H.265", + "忠犬八公的故事.Hachi.A.Dog's.Tale.2009.1080p.BluRay.H.265.mkv", + ); + } + + fn test_scrape_tv2(tag: &str, path: &PathBuf, root: &str, season: &str, episode: &str) { + use soda_resource_tools_lib::soda::entity::{ResourceType, TransferType}; + + init_tracing(); + + default_lib_config(); + let scrape_config = default_scrape_config(); + + let test_dir = tests_dir().join(tag); + clean_dir(&test_dir); + + create_file(&test_dir.join(path)); + + soda::scrape( + ResourceType::MT, + TransferType::Copy, + scrape_config.clone(), + test_dir.to_str().unwrap().to_string(), + test_dir.to_str().unwrap().to_string(), + ); + + let target_file: std::path::PathBuf = test_dir.join(root).join(season).join(episode); + + tracing::debug!("test_scrape_tv target_file = {:?} exist = {}", target_file, target_file.exists()); + + assert_eq!(true, target_file.exists()); + + clean_dir(&test_dir); + } + + fn test_scrape_tv(tag: &str, path: &str, root: &str, season: &str, episode: &str) { + use soda_resource_tools_lib::soda::entity::{ResourceType, TransferType}; + + init_tracing(); + + default_lib_config(); + let scrape_config = default_scrape_config(); + + let test_dir = tests_dir().join(tag); + clean_dir(&test_dir); + + create_file(&test_dir.join(path)); + + soda::scrape( + ResourceType::MT, + TransferType::Copy, + scrape_config.clone(), + test_dir.to_str().unwrap().to_string(), + test_dir.to_str().unwrap().to_string(), + ); + + let target_file: std::path::PathBuf = test_dir.join(root).join(season).join(episode); + + tracing::debug!("test_scrape_tv target_file = {:?} exist = {}", target_file, target_file.exists()); + + assert_eq!(true, target_file.exists()); + + clean_dir(&test_dir); + } + + fn test_scrape_movie(tag: &str, title: &str, root: &str, movie: &str) { + use soda_resource_tools_lib::soda::entity::{ResourceType, TransferType}; + + init_tracing(); + + default_lib_config(); + let scrape_config = default_scrape_config(); + + let test_dir = tests_dir().join(tag); + clean_dir(&test_dir); + + create_file(&test_dir.join(title)); + + soda::scrape( + ResourceType::MT, + TransferType::Copy, + scrape_config.clone(), + test_dir.to_str().unwrap().to_string(), + test_dir.to_str().unwrap().to_string(), + ); + + let target_file: std::path::PathBuf = test_dir.join(root).join(movie); + + assert_eq!(true, target_file.exists()); + + clean_dir(&test_dir); + } + + static mut IS_TRACING_INIT: bool = false; + + /// 初始化日志配置 + fn init_tracing() { + unsafe { + if !IS_TRACING_INIT { + IS_TRACING_INIT = true; + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::DEBUG.into())) + .with_span_events(FmtSpan::FULL) + .init(); + } else { + } + } + } + + fn clean_dir(dir: &PathBuf) { + if dir.exists() { + std::fs::remove_dir_all(dir).unwrap(); + } + } + + fn create_file(file_path: &PathBuf) { + if !file_path.exists() { + let parent = file_path.parent().unwrap(); + if !parent.exists() { + std::fs::create_dir_all(parent).unwrap(); + } + std::fs::File::create(file_path).unwrap(); + } + } + + fn default_scrape_config() -> ScrapeConfig { + let mut config = ScrapeConfig::new(); + config.enable_scrape_image = false; + config.enable_recognize = true; + config + } + + fn default_lib_config() { + let mut config = soda::get_lib_config(); + config.cache_path = tests_dir().join("test_cache").to_string_lossy().to_string(); + config.metadata_skip_special = true; + config.transfer_rename_format_tv = "$title_cn$.$title_en$.$release_year$/$title_cn$.$title_en$.$year$.$season$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$/$title_cn$.$title_en$.$year$.$season$$episode$.$resolution$.$source$.$video_codec$.$audio_codec$-$release_group$.$extension$".to_string(); + config.transfer_rename_format_movie = "$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$/$title_cn$.$title_en$.$year$.$resolution$.$source$.$video_codec$.$audio_codec$.$extension$".to_string(); + soda::update_lib_config(config); + } +} diff --git a/soda_resource_tools_lib/tests/integration_meta_test.rs b/soda_resource_tools_lib/tests/integration_2_meta_test.rs similarity index 75% rename from soda_resource_tools_lib/tests/integration_meta_test.rs rename to soda_resource_tools_lib/tests/integration_2_meta_test.rs index 4d6f1043..20e391db 100644 --- a/soda_resource_tools_lib/tests/integration_meta_test.rs +++ b/soda_resource_tools_lib/tests/integration_2_meta_test.rs @@ -6,6 +6,7 @@ mod meta_tests { use std::io::Read; use serde_json::Value; + use soda_resource_tools_lib::soda::entity::MetaContext; use tracing::Level; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::EnvFilter; @@ -20,22 +21,9 @@ mod meta_tests { /// 初始化日志配置 fn init_tracing() { - // Configure a `tracing` subscriber that logs traces emitted by the server. tracing_subscriber::fmt() - // Filter what traces are displayed based on the RUST_LOG environment - // variable. - // - // Traces emitted by the example code will always be displayed. You - // can set `RUST_LOG=tokio=trace` to enable additional traces emitted by - // Tokio itself. - .with_env_filter(EnvFilter::from_default_env().add_directive(Level::INFO.into())) - // Log events when `tracing` spans are created, entered, exited, or - // closed. When Tokio's internal tracing support is enabled (as - // described above), this can be used to track the lifecycle of spawned - // tasks on the Tokio runtime. + .with_env_filter(EnvFilter::from_default_env().add_directive(Level::DEBUG.into())) .with_span_events(FmtSpan::FULL) - // Set this subscriber as the default, to collect all traces emitted by - // the program. .init(); } @@ -52,16 +40,18 @@ mod meta_tests { // Check if the parsed JSON is an array if let Some(items) = json["items"].as_array() { + let mut scrape_context = MetaContext::new(); + items.iter().rev().for_each(|element| { let title = element["title"].as_str().unwrap(); + scrape_context.init(title); + if !element["metadata"].is_null() { - let metadata = soda::create_mt_metadata(title).unwrap(); + let metadata = soda::meta::create_metadata_mt(&mut scrape_context).unwrap(); if let Some(year) = element["metadata"]["year"].as_str() { - if !year.is_empty() { - assert_eq!(year, metadata.year.unwrap()); - } + assert_eq!(year, metadata.year); } if let Some(season) = element["metadata"]["season"].as_str() { diff --git a/soda_resource_tools_lib/tests/integration_soda_test.rs b/soda_resource_tools_lib/tests/integration_soda_test.rs deleted file mode 100644 index a217249a..00000000 --- a/soda_resource_tools_lib/tests/integration_soda_test.rs +++ /dev/null @@ -1,161 +0,0 @@ -#[cfg(test)] -mod soda_tests { - use std::fs; - use std::path::Path; - - use soda_resource_tools_lib::soda::entity::ScrapeConfig; - use tracing::level_filters::LevelFilter; - use tracing_subscriber::fmt::format::FmtSpan; - use tracing_subscriber::EnvFilter; - - #[test] - fn test_log() { - init_tracing(); - } - - #[cfg(target_os = "windows")] - #[test] - fn test_run_scrape() { - use soda_resource_tools_lib::soda::{ - self, - entity::{ResourceType, TransferType}, - }; - - tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into())).with_span_events(FmtSpan::FULL).init(); - - let mut config = soda::get_lib_config(); - config.strong_match_regex_enable_cache = true; - config.metadata_skip_special = true; - soda::update_lib_config(config); - - let mut scrape_config = ScrapeConfig::new(); - scrape_config.enable_scrape_image = false; - scrape_config.enable_recognize = true; - - // \\NAS-TANK\downloads_disk8\电视剧 - // let src_directory = "D:/Downloads/Src".to_string(); - let src_directory = "\\\\NAS-TANK\\downloads_disk8\\电视剧".to_string(); - // let src_directory = "\\\\NAS-TANK\\downloads_disk8\\电视剧\\北海鲸梦.The.North.Water.E01-E06.2021.1080p.Blu-ray.x265.DTS£cXcY@FRDS".to_string(); - // let src_directory = "\\\\NAS-TANK\\downloads_disk8\\电视剧\\The Big Bang Theory S01-S12 2007-2018 BluRay 1080p Remux DTS-HD MA5.1 AVC-Gamma".to_string(); - // let src_directory = "\\\\NAS-TANK\\downloads_disk8\\电视剧\\The Big Bang Theory S01-S12 2007-2018 BluRay 1080p Remux DTS-HD MA5.1 AVC-Gamma\\S10 2016\\The Big Bang Theory S10E17 The Comic-Con Conundrum 1080p BluRay Remux AVC DTS-HD MA-Gamma.mkv".to_string(); - // let src_directory = "\\\\NAS-TANK\\downloads_disk8\\电视剧\\V世代.Gen.V.S01.2023.1080p.AMZN.WEB-DL.H264.DDP5.1-OurTV\\V世代.Gen.V.S01E07.2023.1080p.AMZN.WEB-DL.H264.DDP5.1-OurTV.mkv".to_string(); - let src_directory = "\\\\NAS-TANK\\downloads_disk8\\电视剧\\消失的十一层.The.Lost.11th.Floor.S01.2023.2160p.WEB-DL.H265.DDP5.1-OurTV".to_string(); - let target_directory = "D:/Downloads/Target".to_string(); - soda::scrape(ResourceType::MT, TransferType::SymbolLink, scrape_config, src_directory, target_directory); - assert_eq!(1, 1); - } - - #[test] - fn test_scrape_mt_hardlink_src_to_target() { - use soda_resource_tools_lib::soda::{ - self, - entity::{ResourceType, TransferType}, - }; - - // init_tracing(); - - let current_path = std::env::current_dir().unwrap(); - tracing::info!("current_path = {:?}", current_path); - - let test_dir = current_path.join("tests").join("test_scrape_mt_hardlink_src_to_target"); - let src_directory = test_dir.join("src").to_str().unwrap().to_string(); - let target_directory = test_dir.join("target").to_str().unwrap().to_string(); - clean_target_directory(&target_directory); - - soda::scrape(ResourceType::MT, TransferType::HardLink, ScrapeConfig::new(), src_directory, target_directory.clone()); - - let target_root = Path::new(&target_directory).join("凡人修仙传.The.Mortal.Ascention"); - let target_season = target_root.join("凡人修仙传.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H.264.AAC-OurTV"); - let target_episode_nfo = target_season.join("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC-OurTV.nfo"); - - assert_eq!(true, Path::new(&target_root.join("tvshow.nfo")).exists()); - assert_eq!(true, Path::new(&target_season.join("season.nfo")).exists()); - assert_eq!(true, Path::new(&target_episode_nfo).exists()); - - let target_episode_jpg = target_season.join("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC-OurTV.jpg"); - assert_eq!(true, Path::new(&target_root.join("backdrop.jpg")).exists()); - assert_eq!(true, Path::new(&target_root.join("background.jpg")).exists()); - assert_eq!(true, Path::new(&target_root.join("banner.jpg")).exists()); - assert_eq!(true, Path::new(&target_root.join("characterart.png")).exists()); - assert_eq!(true, Path::new(&target_root.join("clearart.png")).exists()); - assert_eq!(true, Path::new(&target_root.join("logo.png")).exists()); - assert_eq!(true, Path::new(&target_root.join("poster.jpg")).exists()); - assert_eq!(true, Path::new(&target_root.join("season01-poster.jpg")).exists()); - assert_eq!(true, Path::new(&target_root.join("thumb.jpg")).exists()); - assert_eq!(true, Path::new(&target_episode_jpg).exists()); - } - - #[test] - fn test_scrape_mt_hardlink_src() { - use soda_resource_tools_lib::soda::{ - self, - entity::{ResourceType, TransferType}, - }; - - let current_path = std::env::current_dir().unwrap(); - tracing::info!("current_path = {:?}", current_path); - - let test_dir = current_path.join("tests").join("test_scrape_mt_hardlink_src"); - let src_directory = test_dir.join("src").to_str().unwrap().to_string(); - let target_directory = test_dir.join("target").to_str().unwrap().to_string(); - clean_target_directory(&target_directory); - - soda::scrape(ResourceType::MT, TransferType::HardLink, ScrapeConfig::new(), src_directory.clone(), src_directory.clone()); - - let target_root = Path::new(&src_directory).join("凡人修仙传.The.Mortal.Ascention"); - let target_season = target_root.join("凡人修仙传.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H.264.AAC-OurTV"); - let target_episode_nfo = target_season.join("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC-OurTV.nfo"); - - assert_eq!(true, Path::new(&target_root.join("tvshow.nfo")).exists()); - assert_eq!(true, Path::new(&target_season.join("season.nfo")).exists()); - assert_eq!(true, Path::new(&target_episode_nfo).exists()); - } - - #[test] - fn test_scrape_mt_move_src() { - use soda_resource_tools_lib::soda::{ - self, - entity::{ResourceType, TransferType}, - }; - - let current_path = std::env::current_dir().unwrap(); - tracing::info!("current_path = {:?}", current_path); - - let test_dir = current_path.join("tests").join("test_scrape_mt_move_src"); - let src_directory = test_dir.join("src").to_str().unwrap().to_string(); - let target_directory = test_dir.join("target").to_str().unwrap().to_string(); - let target_root = Path::new(&src_directory).join("凡人修仙传.The.Mortal.Ascention"); - - clean_target_directory(&target_directory); - clean_target_directory(&target_root.to_str().unwrap().to_string()); - - let mut scrape_config = ScrapeConfig::new(); - scrape_config.enable_scrape_image = false; - soda::scrape(ResourceType::MT, TransferType::Move, scrape_config, src_directory.clone(), src_directory.clone()); - - let src_file = Path::new(&src_directory).join("凡人修仙传.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H264.AAC-OurTV").join("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4"); - assert_eq!(true, !src_file.exists()); - - let target_season = target_root.join("凡人修仙传.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H.264.AAC-OurTV"); - let target_episode_nfo = target_season.join("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC-OurTV.nfo"); - - assert_eq!(true, Path::new(&target_root.join("tvshow.nfo")).exists()); - assert_eq!(true, Path::new(&target_season.join("season.nfo")).exists()); - assert_eq!(true, Path::new(&target_episode_nfo).exists()); - - let target_episode = target_season.join("凡人修仙传.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H.264.AAC-OurTV.mp4"); - fs::rename(target_episode, src_file).unwrap(); - } - - /// 初始化日志配置 - fn init_tracing() { - tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into())).with_span_events(FmtSpan::FULL).init(); - } - - fn clean_target_directory(target_directory: &str) { - let target_directory = Path::new(target_directory); - if target_directory.exists() { - std::fs::remove_dir_all(target_directory).unwrap(); - } - } -} diff --git a/soda_resource_tools_lib/tests/movie_and_tv_metadata.json b/soda_resource_tools_lib/tests/movie_and_tv_metadata.json index 2226b486..9abe68b8 100644 --- a/soda_resource_tools_lib/tests/movie_and_tv_metadata.json +++ b/soda_resource_tools_lib/tests/movie_and_tv_metadata.json @@ -120,7 +120,7 @@ "en_name": "Back From The Brink", "year": "2023", "season": "", - "episode": "EP03", + "episode": "E03", "resolution": "2160p", "source": "WEB-DL", "video_codec": "H.265", @@ -149,7 +149,7 @@ "title": "13.Reasons.Why.S01E01.Tape.1.Side.A.2160p.NF.WEB-DL.DDP5.1.HDR.HEVC-HHWEB.mkv", "metadata": { "cn_name": "", - "en_name": "13.Reasons.Why", + "en_name": "13 Reasons Why", "year": "", "season": "S01", "episode": "E01", @@ -226,19 +226,19 @@ } }, { - "title": "Jade.Other People's Money.Ep01.HDTV.1080p.H264-CNHK.ts", + "title": "Jade.Other People's Money.Ep01.HDTV.1080p.H264-CNHK.mkv", "metadata": { "cn_name": "", "en_name": "Other People's Money", "year": "", "season": "", - "episode": "Ep01", + "episode": "E01", "resolution": "1080p", "source": "HDTV", "video_codec": "H.264", "audio_codec": "", "release_group": "CNHK", - "container": "ts" + "container": "mkv" } }, { @@ -706,19 +706,19 @@ } }, { - "title": "AOD.百万同居计划.Million Dollar Family.Ep01.HDTV.1080p.H264-OneHD.ts", + "title": "AOD.百万同居计划.Million Dollar Family.Ep01.HDTV.1080p.H264-OneHD.mkv", "metadata": { "cn_name": "百万同居计划", "en_name": "Million Dollar Family", "year": "", "season": "", - "episode": "Ep01", + "episode": "E01", "resolution": "1080p", "source": "HDTV", "video_codec": "H.264", "audio_codec": "", "release_group": "OneHD", - "container": "ts" + "container": "mkv" } }, { @@ -738,19 +738,19 @@ } }, { - "title": "[壹高清]你喜欢勃拉姆斯吗.Do You Like Brahms.Ep01.HDTV.720p.H264-OneHD.ts", + "title": "[壹高清]你喜欢勃拉姆斯吗.Do You Like Brahms.Ep01.HDTV.720p.H264-OneHD.mkv", "metadata": { "cn_name": "你喜欢勃拉姆斯吗", "en_name": "Do You Like Brahms", "year": "", "season": "", - "episode": "Ep01", + "episode": "E01", "resolution": "720p", "source": "HDTV", "video_codec": "H.264", "audio_codec": "", "release_group": "OneHD", - "container": "ts" + "container": "mkv" } }, { @@ -770,7 +770,7 @@ } }, { - "title": "Jade.Forensic.Heroes.V.E01.2022.HDTV.1080i.H264-HDCTV.ts", + "title": "Jade.Forensic.Heroes.V.E01.2022.HDTV.1080i.H264-HDCTV.mkv", "metadata": { "cn_name": "", "en_name": "Forensic Heroes V", @@ -782,7 +782,7 @@ "video_codec": "H.264", "audio_codec": "", "release_group": "HDCTV", - "container": "ts" + "container": "mkv" } }, { @@ -881,22 +881,6 @@ "container": "mp4" } }, - { - "title": "One.Dream.One.Home.2019.E01.WEB-DL.4K.HLG10.H265.AAC-HDCTV.mp4", - "metadata": { - "cn_name": "", - "en_name": "One Dream One Home", - "year": "2019", - "season": "", - "episode": "E01", - "resolution": "2160p", - "source": "WEB-DL", - "video_codec": "H.265", - "audio_codec": "AAC", - "release_group": "HDCTV", - "container": "mp4" - } - }, { "title": "Scarlet EP01.mp4", "metadata": { @@ -904,7 +888,7 @@ "en_name": "Scarlet", "year": "", "season": "", - "episode": "EP01", + "episode": "E01", "resolution": "", "source": "", "video_codec": "", @@ -952,7 +936,7 @@ "en_name": "Three Kingdoms", "year": "2010", "season": "", - "episode": "EP01", + "episode": "E01", "resolution": "720p", "source": "BluRay", "video_codec": "H.264", @@ -1234,7 +1218,7 @@ } }, { - "title": "Cast.Away.2000.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-BHYS@OurBits.iso", + "title": "Cast.Away.2000.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-BHYS@OurBits.mp4", "metadata": { "cn_name": "", "en_name": "Cast Away", @@ -1246,7 +1230,7 @@ "video_codec": "H.264", "audio_codec": "DTS-HD.MA.5.1", "release_group": "BHYS@OurBits", - "container": "iso" + "container": "mp4" } }, { @@ -1482,7 +1466,7 @@ "season": "", "episode": "", "resolution": "1080p", - "source": "BluRay.Remux", + "source": "BluRay", "video_codec": "H.264", "audio_codec": "TrueHD.7.1", "release_group": "Dream", @@ -1624,7 +1608,7 @@ "en_name": "THE PRIDE OF HUNAN CUISINE", "year": "2021", "season": "", - "episode": "EP01", + "episode": "E01", "resolution": "2160p", "source": "WEB-DL", "video_codec": "H.265", @@ -1640,7 +1624,7 @@ "en_name": "Planet Earth II", "year": "2016", "season": "S01", - "episode": "EP01", + "episode": "E01", "resolution": "2160p", "source": "BluRay.Remux", "video_codec": "H.265", @@ -1653,7 +1637,7 @@ "title": "[大堡礁].BBC.Great.Barrier.Reef.2012.Blu-ray.1080i.AVC.DTS-HD.MA.2.0-CMCT.mkv", "metadata": { "cn_name": "大堡礁", - "en_name": "BBC Great Barrier Reef", + "en_name": "Great Barrier Reef", "year": "2012", "season": "", "episode": "", @@ -1669,7 +1653,7 @@ "title": "[猎捕].The.Hunt.Complete.2015.BluRay.1080p.x264.DTS.2Audios-CMCT.mkv", "metadata": { "cn_name": "猎捕", - "en_name": "The Hunt Complete", + "en_name": "The Hunt", "year": "2015", "season": "", "episode": "", @@ -1688,7 +1672,7 @@ "en_name": "The Hunt", "year": "2015", "season": "", - "episode": "EP01", + "episode": "E01", "resolution": "1080p", "source": "BluRay", "video_codec": "H.264", @@ -1701,7 +1685,7 @@ "title": "[非洲].BBC.Africa.S01.2013.Blu-ray.1080i.AVC.DTS-HD.MA.5.1-CMCT.mkv", "metadata": { "cn_name": "非洲", - "en_name": "BBC Africa", + "en_name": "Africa", "year": "2013", "season": "S01", "episode": "", @@ -1837,7 +1821,7 @@ "season": "S01", "episode": "E01", "resolution": "1080p", - "source": "WEB", + "source": "WEB-DL", "video_codec": "H.264", "audio_codec": "", "release_group": "KOGi", @@ -1880,7 +1864,7 @@ "title": "(2010.05.26)Evangelion 2.22 You Can (Not) Advance-[1080p][JP.BD.Remux].mkv", "metadata": { "cn_name": "", - "en_name": "Evangelion 2.22 You Can (Not) Advance", + "en_name": "Evangelion 2 22 You Can (Not) Advance", "year": "", "season": "", "episode": "", @@ -1934,7 +1918,7 @@ "season": "S01", "episode": "E01", "resolution": "1080p", - "source": "BDRIP", + "source": "BDRip", "video_codec": "H.265", "audio_codec": "FLAC", "release_group": "", @@ -1950,7 +1934,7 @@ "season": "", "episode": "", "resolution": "1080p", - "source": "BDRIP", + "source": "BDRip", "video_codec": "H.264", "audio_codec": "FLAC", "release_group": "", @@ -1964,9 +1948,9 @@ "en_name": "Isekai Ojisan", "year": "", "season": "", - "episode": "03", + "episode": "E03", "resolution": "1080p", - "source": "BDRIP", + "source": "BDRip", "video_codec": "H.264", "audio_codec": "FLAC", "release_group": "", @@ -1980,9 +1964,9 @@ "en_name": "Isekai Ojisan", "year": "", "season": "", - "episode": "01", + "episode": "E01", "resolution": "1080p", - "source": "BDRIP", + "source": "BDRip", "video_codec": "H.264", "audio_codec": "FLACx2", "release_group": "", @@ -2265,7 +2249,7 @@ "title": "消失的十一层.The.Lost.11th.Floor.S01E24.2023.2160p.WEB-DL.H265.DDP5.1-OurTV.mp4", "metadata": { "cn_name": "消失的十一层", - "en_name": "The.Lost.11th.Floor", + "en_name": "The Lost 11th Floor", "year": "2023", "season": "S01", "episode": "E24", @@ -2596,6 +2580,4090 @@ "release_group": "cXcY@FRDS", "container": "mkv" } + }, + { + "title": "The.Ivory.Tower.S01E01.2003.2160p.WEB-DL.H264.60fps.AAC-HHWEB.mp4", + "metadata": { + "cn_name": "", + "en_name": "The Ivory Tower", + "year": "2003", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "HHWEB", + "container": "mp4" + } + }, + { + "title": "Alice.in.Borderland.S01E01.2160p.NF.WEB-DL.DDP5.1.H.265.mkv", + "metadata": { + "cn_name": "", + "en_name": "Alice in Borderland", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "AOD.百万同居计划.Million Dollar Family.Ep15.End.HDTV.1080p.H264-OneHD.mkv", + "metadata": { + "cn_name": "百万同居计划", + "en_name": "Million Dollar Family", + "year": "", + "season": "", + "episode": "E15", + "resolution": "1080p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "OneHD", + "container": "mkv" + } + }, + { + "title": "Arrow.S06E01.Fallout.1080p.Bluray.x265.10bit.DDP.5.1.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Arrow", + "year": "", + "season": "S06", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "[壹高清]你喜欢勃拉姆斯吗.Do You Like Brahms.Ep16.End.HDTV.720p.H264-OneHD.mkv", + "metadata": { + "cn_name": "你喜欢勃拉姆斯吗", + "en_name": "Do You Like Brahms", + "year": "", + "season": "", + "episode": "E16", + "resolution": "720p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "OneHD", + "container": "mkv" + } + }, + { + "title": "Eight.Hours.2022.E01.WEB-DL.4K.H265.DDP.AAC-HDCTV.mp4", + "metadata": { + "cn_name": "", + "en_name": "Eight Hours", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.AAC", + "release_group": "HDCTV", + "container": "mp4" + } + }, + { + "title": "Eight.Hours.2022.E01.Repack.WEB-DL.4K.H265.DDP.AAC-HDCTV.mp4", + "metadata": { + "cn_name": "", + "en_name": "Eight Hours", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.AAC", + "release_group": "HDCTV", + "container": "mp4" + } + }, + { + "title": "From.Repair.to.Pair.2022.E01.WEB-DL.4K.H265.AAC-HDCTV.mp4", + "metadata": { + "cn_name": "", + "en_name": "From Repair to Pair", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "HDCTV", + "container": "mp4" + } + }, + { + "title": "Halo.S01E01.Contact.2160p.WEB-DL.DDP5.1.H.265-NTb.mkv", + "metadata": { + "cn_name": "", + "en_name": "Halo", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "NTb", + "container": "mkv" + } + }, + { + "title": "House.S01E01.2004.Amazon.WEB-DL.1080p.H264.DDP-AREY.mkv", + "metadata": { + "cn_name": "", + "en_name": "House", + "year": "2004", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP", + "release_group": "AREY", + "container": "mkv" + } + }, + { + "title": "Jade.War.of.the.Genders.E100.2000.HDTV.1080i.H264-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "War of the Genders", + "year": "2000", + "season": "", + "episode": "E100", + "resolution": "1080i", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Le.dernier.seigneur.des.Balkans.S01E01.720p.ARTE.WEB-DL.x264.AAC-PTerWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Le dernier seigneur des Balkans", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "720p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "PTerWEB", + "container": "mkv" + } + }, + { + "title": "Love.Between.Fairy.and.Devil.E01.2022.Netflix.WEB-DL.4K.HEVC.DDP-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "Love Between Fairy and Devil", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Shinbun.Kisha.E01.2022.Netflix.WEB-DL.4K.HEVC.HDR.DDP-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "Shinbun Kisha", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Silent.E01.2022.FOD.WEB-DL.1080p.H264.AAC-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "Silent", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Snowpiercer.S01E10.994.Cars.Long.2160p.Netflix.WEB-DL.DDP.5.1.Atmos.HDR.H.265-HHWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Snowpiercer", + "year": "", + "season": "S01", + "episode": "E10", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1.Atmos", + "release_group": "HHWEB", + "container": "mkv" + } + }, + { + "title": "The.Bad.Kids.E01.2020.WEB-DL.4K.H265.AAC-Enichi.mp4", + "metadata": { + "cn_name": "", + "en_name": "The Bad Kids", + "year": "2020", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "Enichi", + "container": "mp4" + } + }, + { + "title": "The.Lying.Life.of.Adults.S01E01.1080p.NF.WEB-DL.x264.DDP5.1-PTerWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Lying Life of Adults", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.5.1", + "release_group": "PTerWEB", + "container": "mkv" + } + }, + { + "title": "The.Peripheral.S01E01.Pilot.2160p.AMZN.WEB-DL.DDP5.1.HDR.H.265-NTb.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Peripheral", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "NTb", + "container": "mkv" + } + }, + { + "title": "TVB Anywhere.法证先锋V.Forensic Heroes V.Ep01.HDTV.1080p.H264.2Audio-OneHD.mkv", + "metadata": { + "cn_name": "法证先锋V", + "en_name": "Forensic Heroes V", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "2Audio", + "release_group": "OneHD", + "container": "mkv" + } + }, + { + "title": "TVB Anywhere.法证先锋V.Forensic Heroes V.Ep01.Repack.HDTV.1080p.H264.2Audio-OneHD.mkv", + "metadata": { + "cn_name": "法证先锋V", + "en_name": "Forensic Heroes V", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "2Audio", + "release_group": "OneHD", + "container": "mkv" + } + }, + { + "title": "Unnatural.2018.E01.BluRay.1080p.x264.DD2.0-HDChina.mkv", + "metadata": { + "cn_name": "", + "en_name": "Unnatural", + "year": "2018", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DD2.0", + "release_group": "HDChina", + "container": "mkv" + } + }, + { + "title": "ViuTV.Million.Dollar.Family.E01.2022.HDTV.1080i.H264-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "Million Dollar Family", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "1080i", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Arrow.S08E08.Crisis.on.Infinite.Earths.Part.Four.1080p.Bluray.x265.10bit.DDP.5.1.MNHDR-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Arrow", + "year": "", + "season": "S08", + "episode": "E08", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "MNHDR-FRDS", + "container": "mkv" + } + }, + { + "title": "Arrow.S08E08.Star.City.2040.1080p.Bluray.x265.10bit.DDP.5.1.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Arrow", + "year": "2040", + "season": "S08", + "episode": "E08", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Arrow.S08E08.Inmate.4587.1080p.Bluray.x265.10bit.DDP.5.1.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Arrow", + "year": "4587", + "season": "S08", + "episode": "E08", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "[大明王朝1566].Da.Ming.Wang.Chao.2007.E01.1080p.WEB-DL.H265.AAC-PTerWEB.mp4", + "metadata": { + "cn_name": "大明王朝1566", + "en_name": "Da Ming Wang Chao", + "year": "2007", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "PTerWEB", + "container": "mp4" + } + }, + { + "title": "[信号].Signal.S01E01.2016.BluRay.1080p.x264.DTS-CMCT.mkv", + "metadata": { + "cn_name": "信号", + "en_name": "Signal", + "year": "2016", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS", + "release_group": "CMCT", + "container": "mkv" + } + }, + { + "title": "Hanzawa.Naoki.S01E01.2013.DC.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Hanzawa Naoki", + "year": "2013", + "season": "S01", + "episode": "E01", + "resolution": "", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "Fringe.2008.S01E01.Blu-Ray.1080p.DD5.1.x265.10bit-Yumi.mkv", + "metadata": { + "cn_name": "", + "en_name": "Fringe", + "year": "2008", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DD5.1", + "release_group": "Yumi", + "container": "mkv" + } + }, + { + "title": "24小时.S01E01.2001.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "24小时", + "en_name": "", + "year": "2001", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "士兵突击.Soldiers.Sortie.2006.E01.2160p.WEB-DL.AAC.H.265-OurTV.mp4", + "metadata": { + "cn_name": "士兵突击", + "en_name": "Soldiers Sortie", + "year": "2006", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "The.World.Between.Us.S01E01.1080p.BluRay.x265.10bit.FLAC.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The World Between Us", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "拜托了!8小时.Eight.Hours.2022.S01E01.1080p.WEB-DL.H264.AAC-OurTV.mp4", + "metadata": { + "cn_name": "拜托了!8小时", + "en_name": "Eight Hours", + "year": "2022", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "The.Lord.of.the.Rings.The.Rings.of.Power.S01.E01.A.Shadow.of.the.Past.1080p.AMZN.WEB-DL.DDP5.1.H.264-OurTV.mp4", + "metadata": { + "cn_name": "", + "en_name": "The Lord of the Rings The Rings of Power", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.5.1", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "Long.Way.Round.2004.S01E01.2160p.ATVP.WEB-DL.H265.10bit.HDR.DD5.1-LeagueWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Long Way Round", + "year": "2004", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DD5.1", + "release_group": "LeagueWEB", + "container": "mkv" + } + }, + { + "title": "Supernatural.S01E13.Route.666.1080p.Blu-Ray.AC3.x265.10bit-Yumi.mkv", + "metadata": { + "cn_name": "", + "en_name": "Supernatural", + "year": "", + "season": "S01", + "episode": "E13", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "Yumi", + "container": "mkv" + } + }, + { + "title": "Marvels.Daredevil.S03E13.2018.2160p.WEB_DL.x265.10bit.AC3£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Marvels Daredevil", + "year": "2018", + "season": "S03", + "episode": "E13", + "resolution": "2160p", + "source": "WEB_DL", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "赘婿.第1季.My.Heroic.Husband.S01E36.2021.2160p.WEB-DL.H265.AAC-FLTTH.mp4", + "metadata": { + "cn_name": "赘婿", + "en_name": "My Heroic Husband", + "year": "2021", + "season": "S01", + "episode": "E36", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "FLTTH", + "container": "mp4" + } + }, + { + "title": "The.Walking.Dead.S01E01.1080p.BluRay.x264-Japhson.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Walking Dead", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "Japhson", + "container": "mkv" + } + }, + { + "title": "2.Broke.Girls.S01E01.2011.1080p.WEB-DL.x265.10bit.AC3£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Broke Girls", + "year": "2011", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "the.walking.dead.s03e01.1080p.bluray.x264-rovers.mkv", + "metadata": { + "cn_name": "", + "en_name": "the walking dead", + "year": "", + "season": "S03", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "rovers", + "container": "mkv" + } + }, + { + "title": "24小时.Legacy.S01E01.2017.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "24小时", + "en_name": "Legacy", + "year": "2017", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "2.Broke.Girls.S04E02.2014.1080p.WEB-DL.x265.10bit.AC3£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Broke Girls", + "year": "2014", + "season": "S04", + "episode": "E02", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "Wild.Bloom.S01E28.2022.2160p.WEB-DL.H265.DDP5.1-HHWEB.mp4.mp4", + "metadata": { + "cn_name": "", + "en_name": "Wild Bloom", + "year": "2022", + "season": "S01", + "episode": "E28", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "HHWEB", + "container": "mp4" + } + }, + { + "title": "钻井.The.Rig.2023.S01E06.1080p.AMZN.WEB-DL.H264.DDP5.1-OurTV.mkv", + "metadata": { + "cn_name": "钻井", + "en_name": "The Rig", + "year": "2023", + "season": "S01", + "episode": "E06", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.5.1", + "release_group": "OurTV", + "container": "mkv" + } + }, + { + "title": "Supernatural.S06E22.The.Man.Who.Knew.Too.Much.Blu-Ray.AC3.x265.10bit-Yumi.mkv", + "metadata": { + "cn_name": "", + "en_name": "Supernatural", + "year": "", + "season": "S06", + "episode": "E22", + "resolution": "", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "Yumi", + "container": "mkv" + } + }, + { + "title": "Supernatural.S06E14.Mannequin.3.The.Reckoning.Blu-Ray.AC3.x265.10bit-Yumi.mkv", + "metadata": { + "cn_name": "", + "en_name": "Supernatural", + "year": "", + "season": "S06", + "episode": "E14", + "resolution": "", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "Yumi", + "container": "mkv" + } + }, + { + "title": "Supernatural.S05E17.99.Problems.1080p.Blu-Ray.AC3.x265.10bit-Yumi.mkv", + "metadata": { + "cn_name": "", + "en_name": "Supernatural", + "year": "", + "season": "S05", + "episode": "E17", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "Yumi", + "container": "mkv" + } + }, + { + "title": "请回答1988.第20集.Reply.1988.UHDTV.60fps.DD2.0.x265.10bit-Yumi.mkv", + "metadata": { + "cn_name": "请回答1988", + "en_name": "Reply 1988", + "year": "", + "season": "", + "episode": "E20", + "resolution": "", + "source": "", + "video_codec": "H.265", + "audio_codec": "DD2.0", + "release_group": "Yumi", + "container": "mkv" + } + }, + { + "title": "the.walking.dead.s11e12.1080p.web.h264-cakes.mkv", + "metadata": { + "cn_name": "", + "en_name": "the walking dead", + "year": "", + "season": "S11", + "episode": "E12", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "cakes", + "container": "mkv" + } + }, + { + "title": "The.Walking.Dead.S02E03.REPACK.1080p.BluRay.x264-BWB.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Walking Dead", + "year": "", + "season": "S02", + "episode": "E03", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "BWB", + "container": "mkv" + } + }, + { + "title": "The.Good.Doctor.S03E06.45-Degree.Angle.1080p.AMZN.WEB-DL.DDP5.1.x265.10bit-Yumi@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Good Doctor", + "year": "", + "season": "S03", + "episode": "E06", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "Yumi@FRDS", + "container": "mkv" + } + }, + { + "title": "The.Big.Bang.Theory.S02E07.The.Panty.Piñata.Polarization.1080p.Bluray.AC3.x265.10bit-Yumi.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Big Bang Theory", + "year": "", + "season": "S02", + "episode": "E07", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "Yumi", + "container": "mkv" + } + }, + { + "title": "2.Broke.Girls.S06E01E02.2016.1080p.WEB-DL.x265.10bit.AC3£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Broke Girls", + "year": "2016", + "season": "S06", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "The.Lord.of.the.Rings.The.Rings.of.Power.S01.E06.Udûn.1080p.AMZN.WEB-DL.DDP5.1.H.264-OurTV.mp4", + "metadata": { + "cn_name": "", + "en_name": "The Lord of the Rings The Rings of Power", + "year": "", + "season": "S01", + "episode": "E06", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.5.1", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "Alice.in.Borderland.S01E01.2160p.NF.WEB-DL.DDP5.1.H.265.mkv", + "metadata": { + "cn_name": "", + "en_name": "Alice in Borderland", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "JSTV.The.Guardian.E01.2022.HDTV.1080i.H264-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Guardian", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "1080i", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Lost.S01-S06.COMPLETE.2004-2010.1080p.Blu-ray.x265.10bits.DTS.5.1-PTer", + "metadata": { + "cn_name": "", + "en_name": "Lost", + "year": "", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS.5.1", + "release_group": "PTer", + "container": "" + } + }, + { + "title": "One.Dream.One.Home.2019.E01.WEB-DL.4K.HLG10.H265.AAC-HDCTV.mp4", + "metadata": { + "cn_name": "", + "en_name": "One Dream One Home", + "year": "2019", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "HDCTV", + "container": "mp4" + } + }, + { + "title": "Remarriage.and.Desires.S01E02.2022.Netflix.WEB-DL.4K.HEVC.DV.DDP-HDCTV.mp4", + "metadata": { + "cn_name": "", + "en_name": "Remarriage and Desires", + "year": "2022", + "season": "S01", + "episode": "E02", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP", + "release_group": "HDCTV", + "container": "mp4" + } + }, + { + "title": "SAS.Rogue.Heroes.S01E01.2022.1080p.Blu-ray.x265.10bit.AC3£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "SAS Rogue Heroes", + "year": "2022", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "Scarlet EP50 End.mp4", + "metadata": { + "cn_name": "", + "en_name": "Scarlet", + "year": "", + "season": "", + "episode": "E50", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "", + "container": "mp4" + } + }, + { + "title": "Shameless.US.S09E01.1080p.AMZN.WEB-DL.AC3.x265.10bit-Yumi@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Shameless", + "year": "", + "season": "S09", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "Yumi@FRDS", + "container": "mkv" + } + }, + { + "title": "Arrow.S06E21.Docket.No.11-19-41-73.1080p.Bluray.x265.10bit.DDP.5.1.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Arrow", + "year": "", + "season": "S06", + "episode": "E21", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "The.Universe.S01E01.Secrets.of.the.Sun.2007.BD-REMUX.1080P.H264.AC3-FrankB.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Universe", + "year": "2007", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BD-Remux", + "video_codec": "H.264", + "audio_codec": "AC3", + "release_group": "FrankB", + "container": "mkv" + } + }, + { + "title": "The.Universe.S01E01.Secrets.of.the.Sun.2007.BD-REMUX.1080P.H264.AVC.AC3-FrankB.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Universe", + "year": "2007", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BD-Remux", + "video_codec": "H.264", + "audio_codec": "AC3", + "release_group": "FrankB", + "container": "mkv" + } + }, + { + "title": "The.Universe.S02E01.Alien.Planets.2008.BD-REMUX.1080P.VC1.DTS-FrankB.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Universe", + "year": "2008", + "season": "S02", + "episode": "E01", + "resolution": "1080p", + "source": "BD-Remux", + "video_codec": "VC1", + "audio_codec": "DTS", + "release_group": "FrankB", + "container": "mkv" + } + }, + { + "title": "The.Universe.S05E01.7.Wonders.of.the.Solar.System.BD-REMUX.1080P.H264.AVC.DTS-HD-FrankB.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Universe", + "year": "", + "season": "S05", + "episode": "E01", + "resolution": "1080p", + "source": "BD-Remux", + "video_codec": "H.264", + "audio_codec": "DTS-HD", + "release_group": "FrankB", + "container": "mkv" + } + }, + { + "title": "The.Universe.S09E01.Omens.of.Doom.1080p.x264.Eac3-FrankB.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Universe", + "year": "", + "season": "S09", + "episode": "E01", + "resolution": "1080p", + "source": "", + "video_codec": "H.264", + "audio_codec": "Eac3", + "release_group": "FrankB", + "container": "mkv" + } + }, + { + "title": "The.Universe.S08E01.2014.stonehenge.1080p.BD-REMUX.H264.AVC.DTS-FrankB.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Universe", + "year": "2014", + "season": "S08", + "episode": "E01", + "resolution": "1080p", + "source": "BD-Remux", + "video_codec": "H.264", + "audio_codec": "DTS", + "release_group": "FrankB", + "container": "mkv" + } + }, + { + "title": "Air.Emergency.S13E01.Fight.to.the.Death.英国欧洲航空548号班机空难.mp4", + "metadata": { + "cn_name": "", + "en_name": "Air Emergency", + "year": "", + "season": "S13", + "episode": "E01", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "", + "container": "mp4" + } + }, + { + "title": "Air.Emergency.S18E01.NutsAndBolts.埃默里世界航空17号班机.1080P[超清.中英字幕].mp4", + "metadata": { + "cn_name": "", + "en_name": "Air Emergency", + "year": "", + "season": "S18", + "episode": "E01", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "", + "container": "mp4" + } + }, + { + "title": "Air.Emergency.S17E01.Killer.Attitude.西北空联航空5719号班机[1080P 中英双语].mp4", + "metadata": { + "cn_name": "", + "en_name": "Air Emergency", + "year": "", + "season": "S17", + "episode": "E01", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "", + "container": "mp4" + } + }, + { + "title": "Air.Emergency.S01E01.Racing.the.Storm.美国航空1420号.1999[高清内封中字].mkv", + "metadata": { + "cn_name": "", + "en_name": "Air Emergency", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "Air.Emergency.S16E01.Deadly.Silence.死寂(1999年里尔35公务机空难).mp4", + "metadata": { + "cn_name": "", + "en_name": "Air Emergency", + "year": "", + "season": "S16", + "episode": "E01", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "", + "container": "mp4" + } + }, + { + "title": "Air.Emergency.S15E01.Fatal.Transmission.1996年昆西机场相撞事故(联合快运航空5925号班机、空中国王N1127D).mp4", + "metadata": { + "cn_name": "", + "en_name": "Air Emergency", + "year": "", + "season": "S15", + "episode": "E01", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "", + "container": "mp4" + } + }, + { + "title": "与摩根·弗里曼一起穿越虫洞.Through.The.Wormhole.With.Morgan.Freeman.2010.S01E01.4K.WEB-DL.H265.AAC-PTerWEB.mp4", + "metadata": { + "cn_name": "与摩根·弗里曼一起穿越虫洞", + "en_name": "Through The Wormhole With Morgan Freeman", + "year": "2010", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "PTerWEB", + "container": "mp4" + } + }, + { + "title": "Great.British.Railway.Journeys.S02E01.HDTV.720p.x264.mkv", + "metadata": { + "cn_name": "", + "en_name": "Great British Railway Journeys", + "year": "", + "season": "S02", + "episode": "E01", + "resolution": "720p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "Great.British.Railway.Journeys.S03E01.HDTV.720p.x264-GTi.mkv", + "metadata": { + "cn_name": "", + "en_name": "Great British Railway Journeys", + "year": "", + "season": "S03", + "episode": "E01", + "resolution": "720p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "GTi", + "container": "mkv" + } + }, + { + "title": "The.Vietnam.War.E01.2017.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Vietnam War", + "year": "2017", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "USA.A.West.Coast.Journey.2014.2160p.BluRay.x265.10bit.SDR.mUHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "A West Coast Journey", + "year": "2014", + "season": "", + "episode": "", + "resolution": "2160p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "mUHD-FRDS", + "container": "mkv" + } + }, + { + "title": "What.on.Earth.S02E01.1080i.HDTV.MPEG2.DD5.1-TTVa.mkv", + "metadata": { + "cn_name": "", + "en_name": "What on Earth", + "year": "", + "season": "S02", + "episode": "E01", + "resolution": "1080i", + "source": "HDTV", + "video_codec": "MPEG2", + "audio_codec": "DD5.1", + "release_group": "TTVa", + "container": "mkv" + } + }, + { + "title": "What.on.Earth.S05E01.CIA.Killer.Monks.1080p.WEB.x264-CAFFEiNE.mkv", + "metadata": { + "cn_name": "", + "en_name": "What on Earth", + "year": "", + "season": "S05", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "CAFFEiNE", + "container": "mkv" + } + }, + { + "title": "What.on.Earth.S08E01.Zombietown.USA.1080p.SCI.WEB-DL.AAC2.0.x264-BOOP.mkv", + "metadata": { + "cn_name": "", + "en_name": "What on Earth", + "year": "", + "season": "S08", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC2.0", + "release_group": "BOOP", + "container": "mkv" + } + }, + { + "title": "What.on.Earth.S09E01.Curse.of.the.Amazon.Nazis.1080p.WEB.h264-CAFFEiNE.mkv", + "metadata": { + "cn_name": "", + "en_name": "What on Earth", + "year": "", + "season": "S09", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "CAFFEiNE", + "container": "mkv" + } + }, + { + "title": "What.on.Earth.S11E01.Secret.Soviet.Spacecraft.1080p.WEB.H264-KOMPOST.mkv", + "metadata": { + "cn_name": "", + "en_name": "What on Earth", + "year": "", + "season": "S11", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "KOMPOST", + "container": "mkv" + } + }, + { + "title": "What.on.Earth.S12E01.1080p.WEB.h264-CBFM.mkv", + "metadata": { + "cn_name": "", + "en_name": "What on Earth", + "year": "", + "season": "S12", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "CBFM", + "container": "mkv" + } + }, + { + "title": "Alone.S02E08.The.Ascent.1080p.AMZN.WEB DL.DD+2.0.x264-Cinefeel.mkv", + "metadata": { + "cn_name": "", + "en_name": "Alone", + "year": "", + "season": "S02", + "episode": "E08", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DD+2.0", + "release_group": "Cinefeel", + "container": "mkv" + } + }, + { + "title": "Alone.S09E11.Fight.Flight.or.Freeze.1080p.AMZN.WEB-DL.AAC2.0.H.264-SMURF.mkv", + "metadata": { + "cn_name": "", + "en_name": "Alone", + "year": "", + "season": "S09", + "episode": "E11", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC2.0", + "release_group": "SMURF", + "container": "mkv" + } + }, + { + "title": "Australia.with.Simon.Reeve.S01E01.WEB-DL.720p.H.264.AAC-FOXKING.mp4", + "metadata": { + "cn_name": "", + "en_name": "Australia with Simon Reeve", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "720p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "FOXKING", + "container": "mp4" + } + }, + { + "title": "BBC.Africa.EP01.Kalahari.2013.1080p.BluRay.x264.DTS-WiKi.mkv", + "metadata": { + "cn_name": "", + "en_name": "Africa", + "year": "2013", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS", + "release_group": "WiKi", + "container": "mkv" + } + }, + { + "title": "Alone.S04E09.My.Brothers.Keeper.1080p.AMZN.WEB-DL.DD+2.0.H.264-AJP69.mkv", + "metadata": { + "cn_name": "", + "en_name": "Alone", + "year": "", + "season": "S04", + "episode": "E09", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DD+2.0", + "release_group": "AJP69", + "container": "mkv" + } + }, + { + "title": "Alone.S05E01.Redemption.1080p.AMZN.WEB-DL.DDP2.0.H.264-KiNGS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Alone", + "year": "", + "season": "S05", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.2.0", + "release_group": "KiNGS", + "container": "mkv" + } + }, + { + "title": "BBC.Africa.2013.S01.EP01.BluRay.1080p.DTS-HD.MA.5.1.x264-beAst.mkv", + "metadata": { + "cn_name": "", + "en_name": "Africa", + "year": "2013", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "beAst", + "container": "mkv" + } + }, + { + "title": "BBC.Attenborough.60.Years.in.the.Wild.2012.EP01.BluRay.1080p.DTS-HD.MA.2.0.x265.10bit-BeiTai.mkv", + "metadata": { + "cn_name": "", + "en_name": "Attenborough 60 Years in the Wild", + "year": "2012", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS-HD.MA.2.0", + "release_group": "BeiTai", + "container": "mkv" + } + }, + { + "title": "BBC.Frozen.Planet.EP01.2011.1080p.BluRay.DTS.3Audio.x264-HDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Frozen Planet", + "year": "2011", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS.3Audio", + "release_group": "HDS", + "container": "mkv" + } + }, + { + "title": "BBC.Inside.the.Human.Body.2011.E01.BluRay.1080p.MultiAudio.DTS-HD.MA.2.0.x265.10bit-BeiTai.mkv", + "metadata": { + "cn_name": "", + "en_name": "Inside the Human Body", + "year": "2011", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS-HD.MA.2.0", + "release_group": "BeiTai", + "container": "mkv" + } + }, + { + "title": "BBC.Life.2009.EP01.BluRay.1080p.MultiAudio.DTS-HD.HR.5.1.x264-beAst.mkv", + "metadata": { + "cn_name": "", + "en_name": "Life", + "year": "2009", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.HR.5.1", + "release_group": "beAst", + "container": "mkv" + } + }, + { + "title": "BBC.Life.on.Earth.1979.EP01.Blu-Ray.1080p.DTS-HD.MA.2.0.x264-beAst.mkv", + "metadata": { + "cn_name": "", + "en_name": "Life on Earth", + "year": "1979", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.2.0", + "release_group": "beAst", + "container": "mkv" + } + }, + { + "title": "BBC.Voyager.To.the.Final.Frontier.1080p.HDTV.x264.AAC.MVGroup.org.mkv", + "metadata": { + "cn_name": "", + "en_name": "Voyager To the Final Frontier", + "year": "", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "MVGroup.org", + "container": "mkv" + } + }, + { + "title": "Blue Planet II 2017 D1 UHD BluRay REMUX 2160p HEVC DTS-HD MA5.1 2Audio-CHD.mkv", + "metadata": { + "cn_name": "", + "en_name": "Blue Planet II", + "year": "2017", + "season": "", + "episode": "", + "resolution": "2160p", + "source": "BluRay.Remux", + "video_codec": "H.265", + "audio_codec": "DTS-HD.MA5.1 2Audio", + "release_group": "CHD", + "container": "mkv" + } + }, + { + "title": "CCTV.Bian.Jiang.Xing.E01.720p.HDTV.x264-NGB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Bian Jiang Xing", + "year": "", + "season": "", + "episode": "E01", + "resolution": "720p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "NGB", + "container": "mkv" + } + }, + { + "title": "CCTV9.Blossom.Matters.E01.1080i.HDTV.H264-NGB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Blossom Matters", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080i", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "NGB", + "container": "mkv" + } + }, + { + "title": "Earth.Flight.Ep01.North.America.1080p.BluRay.x264-xiaofriend.mkv", + "metadata": { + "cn_name": "", + "en_name": "Earth Flight", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "xiaofriend", + "container": "mkv" + } + }, + { + "title": "Forged.in.Fire.S01E01.Japanese.Katana.1080p.AMZN.WEB-DL.DDP2.0.H.264-playWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Forged in Fire", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.2.0", + "release_group": "playWEB", + "container": "mkv" + } + }, + { + "title": "Forged.in.Fire.S05E01.Rookies.Edition.1080p.HULU.WEB-DL.AAC2.0.H.264-SiGMA.mkv", + "metadata": { + "cn_name": "", + "en_name": "Forged in Fire", + "year": "", + "season": "S05", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC2.0", + "release_group": "SiGMA", + "container": "mkv" + } + }, + { + "title": "Formula.1.Drive.to.Survive.S03E01.2021.Netflix.WEB-DL.4K.HEVC.HDR.DDP-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "Formula 1 Drive to Survive", + "year": "2021", + "season": "S03", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Frozen.Planet.II.S01E01.Frozen.Worlds.2160p.iP.WEB-DL.AAC2.0.HLG.H.265-playWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Frozen Planet II", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC2.0", + "release_group": "playWEB", + "container": "mkv" + } + }, + { + "title": "How.Earth.Made.Us.EP01.HK.2010.BluRay.1080P.DTS-HD.MA2.0.x264-th71@beAst.mkv", + "metadata": { + "cn_name": "", + "en_name": "How Earth Made Us", + "year": "2010", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.2.0", + "release_group": "th71@beAst", + "container": "mkv" + } + }, + { + "title": "Night on Earth S01E01 Moonlit Plains 2160p WEBRip DD+5.1 HDR x265-Chotab.mkv", + "metadata": { + "cn_name": "", + "en_name": "Night on Earth", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEBRip", + "video_codec": "H.265", + "audio_codec": "DD+5.1", + "release_group": "Chotab", + "container": "mkv" + } + }, + { + "title": "早餐中国.Breakfast.In.China.S04E31.2020.2160p.WEB-DL.H265.mkv", + "metadata": { + "cn_name": "早餐中国", + "en_name": "Breakfast In China", + "year": "2020", + "season": "S04", + "episode": "E31", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "风味人间.Once.Upon.a.Bite.2018.S01E01.WEB-DL.4K.VP9.AAC-PTerWEB.mkv", + "metadata": { + "cn_name": "风味人间", + "en_name": "Once Upon a Bite", + "year": "2018", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "VP9", + "audio_codec": "AAC", + "release_group": "PTerWEB", + "container": "mkv" + } + }, + { + "title": "Water.Life.2009.E26.1080p.Bluray.x265.10bit.2Audios.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Water Life", + "year": "2009", + "season": "", + "episode": "E26", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audios", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Dynasties.2022.S02E06.V2.1080p.WEB-DL.H264.AAC.2Audio-LeagueWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Dynasties", + "year": "2022", + "season": "S02", + "episode": "E06", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC.2Audio", + "release_group": "LeagueWEB", + "container": "mkv" + } + }, + { + "title": "早餐中国.Breakfast.In.China.S04E31.2020.2160p.WEB-DL.H265.mkv", + "metadata": { + "cn_name": "早餐中国", + "en_name": "Breakfast In China", + "year": "2020", + "season": "S04", + "episode": "E31", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "宇宙时空之旅.Cosmos.A.SpaceTime.Odyssey.S01E01.Standing.Up.in.the.Milky.Way.2014.BluRay.1080p.x265.10Bit.DTS.5.1.内封中字简繁双语特效-FFans@ws林小凡.mkv", + "metadata": { + "cn_name": "宇宙时空之旅", + "en_name": "Cosmos A SpaceTime Odyssey", + "year": "2014", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS.5.1", + "release_group": "FFans@ws林小凡", + "container": "mkv" + } + }, + { + "title": "Around.The.World.In.80.Gardens.2008.E10.1080p.WEB-DL.H264.AAC-PTerWEB.mp4", + "metadata": { + "cn_name": "", + "en_name": "Around The World In 80 Gardens", + "year": "2008", + "season": "", + "episode": "E10", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "PTerWEB", + "container": "mp4" + } + }, + { + "title": "History.of.the.World.2008.S01E100.2160p.WEB-DL.H265.AAC-LeagueWEB.mp4", + "metadata": { + "cn_name": "", + "en_name": "History of the World", + "year": "2008", + "season": "S01", + "episode": "E100", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "LeagueWEB", + "container": "mp4" + } + }, + { + "title": "Into.the.Universe.With.Stephen.Hawking.2010.E03.1080p.Bluray.x265.10bit.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Into the Universe With Stephen Hawking", + "year": "2010", + "season": "", + "episode": "E03", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Turkey.with.Simon.Reeve.S01E01.WEB-DL.1080p.H.264.AAC-FOXKING.mp4", + "metadata": { + "cn_name": "", + "en_name": "Turkey with Simon Reeve", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "FOXKING", + "container": "mp4" + } + }, + { + "title": "The.Civil.War.E9.1865.The.Better.Angels.of.Our.Nature.720p.BluRay.DD5.1.x264-DON.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Civil War", + "year": "1865", + "season": "", + "episode": "E09", + "resolution": "720p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DD5.1", + "release_group": "DON", + "container": "mkv" + } + }, + { + "title": "Planet.Earth.EP01.From.Polo.To.Polo.2006.BluRay.1080p.MultiAudio.DTS-HD.HR.5.1.x264-beAst.mkv", + "metadata": { + "cn_name": "", + "en_name": "Planet Earth", + "year": "2006", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.HR.5.1", + "release_group": "beAst", + "container": "mkv" + } + }, + { + "title": "Mediterranean.with.Simon.Reeve.S01E4.WEB-DL.1080p.H.264.AAC-FOXKING.mp4", + "metadata": { + "cn_name": "", + "en_name": "Mediterranean with Simon Reeve", + "year": "", + "season": "S01", + "episode": "E04", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "FOXKING", + "container": "mp4" + } + }, + { + "title": "Wonders.Of.The.Solar.System.EP01.Empire.of.the.Sun.2010.1080p.BluRay.DD2.0.x264-HDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Wonders Of The Solar System", + "year": "2010", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DD2.0", + "release_group": "HDS", + "container": "mkv" + } + }, + { + "title": "Elon.Musk.The.Real.Life.Iron.Man.2018.1080p.AMZN.WEB-DL.AAC2.0.H.264-ETHiCS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Elon Musk The Real Life Iron Man", + "year": "2018", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC2.0", + "release_group": "ETHiCS", + "container": "mkv" + } + }, + { + "title": "Forged.in.Fire.S08E01.720p.WEB.h264-BAE.mkv", + "metadata": { + "cn_name": "", + "en_name": "Forged in Fire", + "year": "", + "season": "S08", + "episode": "E01", + "resolution": "720p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "BAE", + "container": "mkv" + } + }, + { + "title": "Great.British.Railway.Journeys.S03E25.WEBRIP.720p.x264-iPRiP.mkv", + "metadata": { + "cn_name": "", + "en_name": "Great British Railway Journeys", + "year": "", + "season": "S03", + "episode": "E25", + "resolution": "720p", + "source": "WEBRIP", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "iPRiP", + "container": "mkv" + } + }, + { + "title": "Great.British.Railway.Journeys.S08E01.HDTV.720p.AC3.AAC.x264-GTi.mkv", + "metadata": { + "cn_name": "", + "en_name": "Great British Railway Journeys", + "year": "", + "season": "S08", + "episode": "E01", + "resolution": "720p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "AC3.AAC", + "release_group": "GTi", + "container": "mkv" + } + }, + { + "title": "Great.British.Railway.Journeys.S10E13.Ealing.Broadway.To.South.Kensington.720p.HDTV.x264-BRiTiSHB00Bs.mkv", + "metadata": { + "cn_name": "", + "en_name": "Great British Railway Journeys", + "year": "", + "season": "S10", + "episode": "E13", + "resolution": "720p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "BRiTiSHB00Bs", + "container": "mkv" + } + }, + { + "title": "Great.British.Railway.Journeys.S11E07.Truro.To.St.Mawgan.720p.HDTV.x264-PVR.mkv", + "metadata": { + "cn_name": "", + "en_name": "Great British Railway Journeys", + "year": "", + "season": "S11", + "episode": "E07", + "resolution": "720p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "PVR", + "container": "mkv" + } + }, + { + "title": "What.on.Earth.S07E05.Armageddon.USA.1080p.WEB.x264-ESPRESSO.mkv", + "metadata": { + "cn_name": "", + "en_name": "What on Earth", + "year": "", + "season": "S07", + "episode": "E05", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "ESPRESSO", + "container": "mkv" + } + }, + { + "title": "Dynasties.2022.S02E01.V2.2160p.WEB-DL.H264.DDP5.1.Atmos.2Audio-LeagueWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Dynasties", + "year": "2022", + "season": "S02", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.5.1.Atmos.2Audio", + "release_group": "LeagueWEB", + "container": "mkv" + } + }, + { + "title": "Immigration.Nation.2020.S01E01.Installing.Fear.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", + "metadata": { + "cn_name": "", + "en_name": "Immigration Nation", + "year": "2020", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.5.1", + "release_group": "NTG", + "container": "mkv" + } + }, + { + "title": "Our Planet 2019 S01E04 Coastal Seas 2160p NF WEBRip x265 .mkv", + "metadata": { + "cn_name": "", + "en_name": "Our Planet", + "year": "2019", + "season": "S01", + "episode": "E04", + "resolution": "2160p", + "source": "WEBRip", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "Our Planet 2019 S01E06 - The High Seas 2160p NF WEBRip x265 .mkv", + "metadata": { + "cn_name": "", + "en_name": "Our Planet", + "year": "2019", + "season": "S01", + "episode": "E06", + "resolution": "2160p", + "source": "WEBRip", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "Taiwan.Taste.S01E07.2012.1080p.KKTV.WEB-DL.H264.AAC-HHWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Taiwan Taste", + "year": "2012", + "season": "S01", + "episode": "E07", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "HHWEB", + "container": "mkv" + } + }, + { + "title": "Dynasties.2022.S02E01.V2.2160p.WEB-DL.H264.DDP5.1.Atmos.2Audio-LeagueWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Dynasties", + "year": "2022", + "season": "S02", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.5.1.Atmos.2Audio", + "release_group": "LeagueWEB", + "container": "mkv" + } + }, + { + "title": "[易中天品三国52千古风流].Yi.Zhong.Tian.Pin.San.Guo.E52.2006.DVDRip.576p.x264.AC3-CMCT.mkv", + "metadata": { + "cn_name": "易中天品三国", + "en_name": "Yi Zhong Tian Pin San Guo", + "year": "2006", + "season": "", + "episode": "E52", + "resolution": "576p", + "source": "DVDRip", + "video_codec": "H.264", + "audio_codec": "AC3", + "release_group": "CMCT", + "container": "mkv" + } + }, + { + "title": "风味人间.Once.Upon.A.Bite.2022.S04E01.2160p.50FPS.WEB-DL.H265.DV.DDP2.0-OurTV.mp4", + "metadata": { + "cn_name": "风味人间", + "en_name": "Once Upon A Bite", + "year": "2022", + "season": "S04", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.2.0", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "风味人间3·大海小鲜.Once.Upon.A.Bite.2021.S03E01.4K.WEB-DL.H265.DoVi.DDP5.1.Atmos-PTerWEB.mp4", + "metadata": { + "cn_name": "风味人间", + "en_name": "Once Upon A Bite", + "year": "2021", + "season": "S03", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1.Atmos", + "release_group": "PTerWEB", + "container": "mp4" + } + }, + { + "title": "风味人间4.Once.Upon.A.Bite.2022.S04E01.2160p.50FPS.WEB-DL.H265.DV.DDP2.0-OurTV.mp4", + "metadata": { + "cn_name": "风味人间", + "en_name": "Once Upon A Bite", + "year": "2022", + "season": "S04", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.2.0", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "风味人间4.Once.Upon.A.Bite.2022.S04E01.2160p.50FPS.WEB-DL.H265.DV.DDP2.0-OurTV.mp4", + "metadata": { + "cn_name": "风味人间", + "en_name": "Once Upon A Bite", + "year": "2022", + "season": "S04", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.2.0", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "Re.Zero.kara.Hajimeru.Isekai.Seikatsu.E26.Crunchyroll.WEB-DL.1080p.x264.AAC-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "Re Zero kara Hajimeru Isekai Seikatsu", + "year": "", + "season": "", + "episode": "E26", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Record.of.Ragnarok.II.2023.S02E01.1080p.NF.WEB-DL.DDP2.0.x264-PTerWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Record of Ragnarok II", + "year": "2023", + "season": "S02", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DDP.2.0", + "release_group": "PTerWEB", + "container": "mkv" + } + }, + { + "title": "Meitantei.Conan.1996.E1017.2160p.WEB-DL.H265.AAC.2Audio-HaresWEB.mkv", + "metadata": { + "cn_name": "", + "en_name": "Meitantei Conan", + "year": "1996", + "season": "", + "episode": "E1017", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC.2Audio", + "release_group": "HaresWEB", + "container": "mkv" + } + }, + { + "title": "孤独摇滚.Bocchi.the.Rock.2022.S01E01.1080p.Crunchyroll.WEB-DL.H264.AAC-OurTV.mkv", + "metadata": { + "cn_name": "孤独摇滚", + "en_name": "Bocchi the Rock", + "year": "2022", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "OurTV", + "container": "mkv" + } + }, + { + "title": "斗罗大陆.Dou.Luo.Da.Lu.2018.S01E001.1080p.WEB-DL.AAC.H264-OurTV.mp4", + "metadata": { + "cn_name": "斗罗大陆", + "en_name": "Dou Luo Da Lu", + "year": "2018", + "season": "S01", + "episode": "E001", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "Cyberpunk.Edgerunners.E02.Like.A.Boy.2160p.NF.Blu-ray.DDP5.1.H.264-SPADE.mkv", + "metadata": { + "cn_name": "", + "en_name": "Cyberpunk Edgerunners", + "year": "", + "season": "", + "episode": "E02", + "resolution": "2160p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DDP.5.1", + "release_group": "SPADE", + "container": "mkv" + } + }, + { + "title": "Cyberpunk.Edgerunners.02.Like.A.Boy.2160p.NF.Blu-ray.DDP5.1.H.264-SPADE.mkv", + "metadata": { + "cn_name": "", + "en_name": "Cyberpunk Edgerunners", + "year": "", + "season": "", + "episode": "E02", + "resolution": "2160p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DDP.5.1", + "release_group": "SPADE", + "container": "mkv" + } + }, + { + "title": "鬼灭之刃.锻刀村篇.Kimetsu.No.Yaiba.S04E01.2023.1080p.Crunchyroll.WEB-DL.H264.AAC-OurTV.mkv", + "metadata": { + "cn_name": "鬼灭之刃.锻刀村篇", + "en_name": "Kimetsu No Yaiba", + "year": "2023", + "season": "S04", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "OurTV", + "container": "mkv" + } + }, + { + "title": "[VCB-Studio] OVERLORD [13][Ma10p_1080p][x265_flac].mkv", + "metadata": { + "cn_name": "", + "en_name": "OVERLORD", + "year": "", + "season": "", + "episode": "E13", + "resolution": "1080p", + "source": "", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "VCB-Studio", + "container": "mkv" + } + }, + { + "title": "[VCB-Studio] OVERLORD III [02][Ma10p_1080p][x265_flac].mkv", + "metadata": { + "cn_name": "", + "en_name": "OVERLORD III", + "year": "", + "season": "", + "episode": "E02", + "resolution": "1080p", + "source": "", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "VCB-Studio", + "container": "mkv" + } + }, + { + "title": "[VCB-Studio] OVERLORD III [01][Ma10p_1080p][x265_flac_aac].mkv", + "metadata": { + "cn_name": "", + "en_name": "OVERLORD III", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "", + "video_codec": "H.265", + "audio_codec": "FLAC.AAC", + "release_group": "VCB-Studio", + "container": "mkv" + } + }, + { + "title": "[VCB-Studio] Kaifuku Jutsushi no Yarinaoshi [01][Ma10p_1080p][x265_flac].mkv", + "metadata": { + "cn_name": "", + "en_name": "Kaifuku Jutsushi no Yarinaoshi", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "VCB-Studio", + "container": "mkv" + } + }, + { + "title": "[VCB-Studio] Durarara!!×2 Shou [01][Ma10p_1080p][x265_flac_aac].mkv", + "metadata": { + "cn_name": "", + "en_name": "Durarara!!×2 Shou", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "", + "video_codec": "H.265", + "audio_codec": "FLAC.AAC", + "release_group": "VCB-Studio", + "container": "mkv" + } + }, + { + "title": "[VCB-Studio] Durarara!! [01][Ma10p_1080p][x265_flac].mkv", + "metadata": { + "cn_name": "", + "en_name": "Durarara!!", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "VCB-Studio", + "container": "mkv" + } + }, + { + "title": "Dragon.Ball.150.DBOX.CC.480p.x264-SoM.mkv", + "metadata": { + "cn_name": "", + "en_name": "Dragon Ball", + "year": "", + "season": "", + "episode": "E150", + "resolution": "480p", + "source": "", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "SoM", + "container": "mkv" + } + }, + { + "title": "Dragon.Ball.092.DBOX.CC.480p.x264-SoM.[v2].mkv", + "metadata": { + "cn_name": "", + "en_name": "Dragon Ball", + "year": "", + "season": "", + "episode": "E92", + "resolution": "480p", + "source": "", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "SoM", + "container": "mkv" + } + }, + { + "title": "[sergey_krs] Kuroshitsuji II - 01 [BDRip 1920x1080 x264 FLAC].mkv", + "metadata": { + "cn_name": "", + "en_name": "Kuroshitsuji II", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BDRip", + "video_codec": "H.264", + "audio_codec": "FLAC", + "release_group": "sergey_krs", + "container": "mkv" + } + }, + { + "title": "[sergey_krs] Kuroshitsuji - Book of Circus - 01 [BDRip 1920x1080 x264 FLAC].mkv", + "metadata": { + "cn_name": "", + "en_name": "Kuroshitsuji Book of Circus", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BDRip", + "video_codec": "H.264", + "audio_codec": "FLAC", + "release_group": "sergey_krs", + "container": "mkv" + } + }, + { + "title": "[philosophy-raws][Heroic Age][26][BDRIP][Hi10P FLAC][1920X1080].mkv", + "metadata": { + "cn_name": "", + "en_name": "Heroic Age", + "year": "", + "season": "", + "episode": "E26", + "resolution": "1080p", + "source": "BDRip", + "video_codec": "", + "audio_codec": "FLAC", + "release_group": "philosophy-raws", + "container": "mkv" + } + }, + { + "title": "[Nekomoe kissaten&VCB-Studio] Mushoku Tensei ~Isekai Ittara Honki Dasu~ [01][Ma10p_1080p][x265_flac].mkv", + "metadata": { + "cn_name": "", + "en_name": "Mushoku Tensei ~Isekai Ittara Honki Dasu~", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "Nekomoe kissaten&VCB-Studio", + "container": "mkv" + } + }, + { + "title": "[NC-Raws] OVERLORD IV - 01 (B-Global 3840x2160 HEVC AAC MKV) [038B942D].mkv", + "metadata": { + "cn_name": "", + "en_name": "OVERLORD IV", + "year": "", + "season": "", + "episode": "E01", + "resolution": "2160p", + "source": "", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "NC-Raws", + "container": "mkv" + } + }, + { + "title": "[DMG] EDENS ZERO 第01话「桜舞うソラに」 [BDRip][HEVC_FLAC][1080P_Ma10P](3E87A76C).mkv", + "metadata": { + "cn_name": "", + "en_name": "EDENS ZERO", + "year": "", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "BDRip", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "DMG", + "container": "mkv" + } + }, + { + "title": "Mobseka 2022 S01E01-[1080p][BDRIP][x265.FLAC].mkv", + "metadata": { + "cn_name": "", + "en_name": "Mobseka", + "year": "2022", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BDRip", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "Onipan! S01E01-[1080p][BDRIP][x265.FLAC].mkv", + "metadata": { + "cn_name": "", + "en_name": "Onipan!", + "year": "", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "BDRip", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "Zom.100.Bucket.List.of.the.Dead.S01E01.2023.2160p.B-Global.WEB-DL.H264.AAC-OurTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "Zom 100 Bucket List of the Dead", + "year": "2023", + "season": "S01", + "episode": "E01", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "OurTV", + "container": "mkv" + } + }, + { + "title": "okyo.24.ku.S01E01.2022.1080p.Crunchyroll.WEB-DL.H264.AAC-ADWeb.mkv", + "metadata": { + "cn_name": "", + "en_name": "okyo 24 ku", + "year": "2022", + "season": "S01", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "ADWeb", + "container": "mkv" + } + }, + { + "title": "SPY.x.FAMILY.E01.2022.Crunchyroll.WEB-DL.1080p.x264.AAC-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "SPY x FAMILY", + "year": "2022", + "season": "", + "episode": "E01", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Re.Zero.kara.Hajimeru.Isekai.Seikatsu.E50.Crunchyroll.WEB-DL.1080p.x264.AAC-HDCTV.mkv", + "metadata": { + "cn_name": "", + "en_name": "Re Zero kara Hajimeru Isekai Seikatsu", + "year": "", + "season": "", + "episode": "E50", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "AAC", + "release_group": "HDCTV", + "container": "mkv" + } + }, + { + "title": "Hunter X Hunter 2011 - EP001 [BD 1920x1080 23.976fps AVC-yuv444p10 FLAC Chap] v2 - mawen1250.mkv", + "metadata": { + "cn_name": "", + "en_name": "Hunter X Hunter", + "year": "2011", + "season": "", + "episode": "E001", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "mawen1250", + "container": "mkv" + } + }, + { + "title": "圣斗士星矢第52话:亚历士,传说中的魔皇拳.mkv", + "metadata": { + "cn_name": "圣斗士星矢", + "en_name": "", + "year": "", + "season": "", + "episode": "E52", + "resolution": "", + "source": "", + "video_codec": "", + "audio_codec": "", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "电锯惊魂10.saw.x.2023.1080p.WEB-DL.DD5.1.H264-OurTV.mkv", + "metadata": { + "cn_name": "电锯惊魂10", + "en_name": "saw x", + "year": "2023", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DD5.1", + "release_group": "OurTV", + "container": "mkv" + } + }, + { + "title": "消失的她.Lost.in.the.Stars.2022.WEB-DL.2160p.HEVC.AAC-ZmWeb-高码率.mp4", + "metadata": { + "cn_name": "消失的她", + "en_name": "Lost in the Stars", + "year": "2022", + "season": "", + "episode": "", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "ZmWeb", + "container": "mp4" + } + }, + { + "title": "流浪地球2.The.Wandering.Earth.Ⅱ.2023.2160p.HQ.WEB-DL.H265.DDP5.1.Atoms-OurTV.mp4", + "metadata": { + "cn_name": "流浪地球2", + "en_name": "The Wandering Earth Ⅱ", + "year": "2023", + "season": "", + "episode": "", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1.Atoms", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "扫毒3:人在天涯.The.White.Storm.3.Heaven.or.Hell.2023.2160p.WEB-DL.H265.10bit.DDP5.1.2Audio-OurTV.mp4", + "metadata": { + "cn_name": "扫毒3:人在天涯", + "en_name": "The White Storm 3 Heaven or Hell", + "year": "2023", + "season": "", + "episode": "", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1.2Audio", + "release_group": "OurTV", + "container": "mp4" + } + }, + { + "title": "[封神第一部:朝歌风云].Creation.of.the.Gods.2023.2160p.120fps.WEB-DL.HEVC.10bit.DTSE5.1.3Audios-QHstudIo.mp4", + "metadata": { + "cn_name": "封神第一部:朝歌风云", + "en_name": "Creation of the Gods", + "year": "2023", + "season": "", + "episode": "", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DTSE5.1.3Audios", + "release_group": "QHstudIo", + "container": "mp4" + } + }, + { + "title": "The Imitation Game 2014 BluRay Remux 1080p AVC DTS-HD MA 5.1-OurBits.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Imitation Game", + "year": "2014", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "OurBits", + "container": "mkv" + } + }, + { + "title": "Vicky Cristina Barcelona 2008 1080p Bluray Remux VC-1 DTS-HD 3.0-decatora27.mkv", + "metadata": { + "cn_name": "", + "en_name": "Vicky Cristina Barcelona", + "year": "2008", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "VC-1", + "audio_codec": "DTS-HD.3.0", + "release_group": "decatora27", + "container": "mkv" + } + }, + { + "title": "Sing 2016 1080p BluRay DD 5 1 x264-FuzerHD.mkv", + "metadata": { + "cn_name": "", + "en_name": "Sing", + "year": "2016", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DD 5 1", + "release_group": "FuzerHD", + "container": "mkv" + } + }, + { + "title": "Isle Of Dogs 2018 BluRay 1080p HEVC (10bit) DTS-HD MA 5.1 x265-LEGi0N.mkv", + "metadata": { + "cn_name": "", + "en_name": "Isle Of Dogs", + "year": "2018", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "LEGi0N", + "container": "mkv" + } + }, + { + "title": "Ant Man 2015 UHD BluRay REMUX 2160p HEVC Atmos TrueHD 7.1-PTer.mkv", + "metadata": { + "cn_name": "", + "en_name": "Ant Man", + "year": "2015", + "season": "", + "episode": "", + "resolution": "2160p", + "source": "BluRay.Remux", + "video_codec": "H.265", + "audio_codec": "Atmos TrueHD.7.1", + "release_group": "PTer", + "container": "mkv" + } + }, + { + "title": "阿甘正传.Forrest.Gump.1994.Remastered.1080p.Blu-ray.x265.10bit.DTS£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "阿甘正传", + "en_name": "Forrest Gump", + "year": "1994", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "绿里奇迹.The.Green.Mile.1999.JPN.1080p.Blu-ray.x265.10bit.DTS£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "绿里奇迹", + "en_name": "The Green Mile", + "year": "1999", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "Oceans 2009.CHN.Bluray.1080p.x265.10bit.2Audios.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Oceans", + "year": "2009", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audios", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "12.Angry.Men.1957.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "12 Angry Men", + "year": "1957", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "CCTV6-HD.The.Teahouse.1982.1080p.HDTV.x264.DD2.0-LTF.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Teahouse", + "year": "1982", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "HDTV", + "video_codec": "H.264", + "audio_codec": "DD2.0", + "release_group": "LTF", + "container": "mkv" + } + }, + { + "title": "2001.A.Space.Odyssey.1968.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "2001 A Space Odyssey", + "year": "1968", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Top138.英雄本色(4K修复版).A.Better.Tomorrow.1986.REMASTERED.Bluray.1080p.x265.AAC(5.1).2Audios.GREENOTEA.mkv", + "metadata": { + "cn_name": "英雄本色", + "en_name": "A Better Tomorrow", + "year": "1986", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AAC(5.1).2Audios", + "release_group": "GREENOTEA", + "container": "mkv" + } + }, + { + "title": "Black Adam 2022 BluRay Remux 1080p AVC Atmos TrueHD 7.1-OurBits.mkv", + "metadata": { + "cn_name": "", + "en_name": "Black Adam", + "year": "2022", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "Atmos TrueHD.7.1", + "release_group": "OurBits", + "container": "mkv" + } + }, + { + "title": "The_Last_Thing_He_Wanted_2020_INTERNAL_1080p_WEB_X264-AMRAP.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Last Thing He Wanted", + "year": "2020", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "AMRAP", + "container": "mkv" + } + }, + { + "title": "Phantoms 1998 BluRay 1080p AC3 HEVC-d3g.mkv", + "metadata": { + "cn_name": "", + "en_name": "Phantoms", + "year": "1998", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "AC3", + "release_group": "d3g", + "container": "mkv" + } + }, + { + "title": "Pearl Harbor 2001 1080p BluRay DTS-HD MA 5.1 x264-BluEvo.mkv", + "metadata": { + "cn_name": "", + "en_name": "Pearl Harbor", + "year": "2001", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "BluEvo", + "container": "mkv" + } + }, + { + "title": "Jay and Silent Bob Strike Back 2001 1080p BluRay DTS x264-LEGi0N.mkv", + "metadata": { + "cn_name": "", + "en_name": "Jay and Silent Bob Strike Back", + "year": "2001", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS", + "release_group": "LEGi0N", + "container": "mkv" + } + }, + { + "title": "Good Will Hunting 1997 REMUX BluRay 1080p AVC DTS-HD MA 5.1-LEGi0N.mkv", + "metadata": { + "cn_name": "", + "en_name": "Good Will Hunting", + "year": "1997", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "LEGi0N", + "container": "mkv" + } + }, + { + "title": "Boiler Room 2000 REMUX 1080p Blu-ray AVC DTS-HD MA 7 1-LEGi0N.mkv", + "metadata": { + "cn_name": "", + "en_name": "Boiler Room", + "year": "2000", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.7 1", + "release_group": "LEGi0N", + "container": "mkv" + } + }, + { + "title": "Spider-Man.Across.the.Spider-Verse.2023.2160p.MA.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX.mkv", + "metadata": { + "cn_name": "", + "en_name": "Spider Man Across the Spider Verse", + "year": "2023", + "season": "", + "episode": "", + "resolution": "2160p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "DDP.5.1.Atmos", + "release_group": "FLUX", + "container": "mkv" + } + }, + { + "title": "Once Upon a Time in America 1984 Extended BluRay REMUX 1080p VC-1 DTS-HD MA5.1 4Audio-CHD.mkv", + "metadata": { + "cn_name": "", + "en_name": "Once Upon a Time in America", + "year": "1984", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "VC-1", + "audio_codec": "DTS-HD.MA5.1 4Audio", + "release_group": "CHD", + "container": "mkv" + } + }, + { + "title": "Suzume.2022.1080p.iTunes.2Audio.WEB-DL.DD5.1.H.264.mkv", + "metadata": { + "cn_name": "", + "en_name": "Suzume", + "year": "2022", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DD5.1", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "Argo.2012.BluRay.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR.mkv", + "metadata": { + "cn_name": "", + "en_name": "Argo", + "year": "2012", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "FraMeSToR", + "container": "mkv" + } + }, + { + "title": "Armageddon.1998.1080p.Blu-ray.Remux.AVC.DTS-HD.MA.5.1-KRaLiMaRKo.mkv", + "metadata": { + "cn_name": "", + "en_name": "Armageddon", + "year": "1998", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "KRaLiMaRKo", + "container": "mkv" + } + }, + { + "title": "Wreck-It.Ralph.2012.Bluray.1080p.x265.10bit.4Audios.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Wreck It Ralph", + "year": "2012", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "4Audios", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Wreck-It.Ralph2012.Bluray.1080p.x265.10bit.4Audios.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Wreck It Ralph", + "year": "2012", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "4Audios", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "The.Trueman.Show.1998.BluRay.1080p.x265.10bit.3Audio.MnHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Trueman Show", + "year": "1998", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "3Audio", + "release_group": "MnHD-FRDS", + "container": "mkv" + } + }, + { + "title": "SP.BURN∙E.2008.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "BURN E", + "year": "2008", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audio", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "New.Dragon.Inn.1992.1080p.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "New Dragon Inn", + "year": "1992", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audio", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "The.Lord.of.the.Rings.The.Fellowship.of.the.Ring.2001.EE.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Lord of the Rings The Fellowship of the Ring", + "year": "2001", + "season": "", + "episode": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audio", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Saving.Private.Ryan.1998.BluRay.1080p.x265.10bit.4Audio.MnHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Saving Private Ryan", + "year": "1998", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "4Audio", + "release_group": "MnHD-FRDS", + "container": "mkv" + } + }, + { + "title": "The.Blind.Side.2009.BluRay.720p.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Blind Side", + "year": "2009", + "resolution": "720p", + "source": "BluRay", + "video_codec": "", + "audio_codec": "", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "The.Great.Buddha+2017.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Great Buddha", + "year": "2017", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Edward.Scissorhands.25th.Anniversary.Remastered.Edition.BluRay.1080p.x265.10bit.2Audios.MNHD-FRD.mkv", + "metadata": { + "cn_name": "", + "en_name": "Edward Scissorhands", + "year": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audios", + "release_group": "MNHD-FRD", + "container": "mkv" + } + }, + { + "title": "Gone.with.the.Wind.1939.BluRay.1080p.x265.10bit.9Audio.MnHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Gone with the Wind", + "year": "1939", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "9Audio", + "release_group": "MnHD-FRDS", + "container": "mkv" + } + }, + { + "title": "A.One.and.a.Two.2000.CC.BluRay.1080p.x265.10bit.3Audio-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "A One and a Two", + "year": "2000", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "3Audio", + "release_group": "FRDS", + "container": "mkv" + } + }, + { + "title": "Late.Blossom.2011.1080p.WEB-DL.H265.AAC-PTHweb.mp4", + "metadata": { + "cn_name": "", + "en_name": "Late Blossom", + "year": "2011", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "PTHweb", + "container": "mp4" + } + }, + { + "title": "The.Eagle.Shooting.Heroes.1993.1080p.KOREA.BluRay.x265.10bit.DD+5.1.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Eagle Shooting Heroes", + "year": "1993", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DD+5.1", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Prince.Nezha's.Triumph.Against.Dragon.King.1979.Webrip.1080p.x265.10bit.AAC.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Prince Nezha's Triumph Against Dragon King", + "year": "1979", + "resolution": "1080p", + "source": "Webrip", + "video_codec": "H.265", + "audio_codec": "AAC", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Avatar.2009.Extended.Collector's.Edition.Hybrid.BluRay.1080p.x265.10bit.DDP5.1.Repack.H.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Avatar", + "year": "2009", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Batman.v.Superman.Dawn.of.Justice.2016.1080p.TW.Bluray.REMUX.AVC.TrueHD.Atmos 7.1-HDPter.mkv", + "metadata": { + "cn_name": "", + "en_name": "Batman v Superman Dawn of Justice", + "year": "2016", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "TrueHD.Atmos 7.1", + "release_group": "HDPter", + "container": "mkv" + } + }, + { + "title": "Bounce.2000.1080p.AMZN.WEB-DL.DD+5.1.x264-VLAD.mkv", + "metadata": { + "cn_name": "", + "en_name": "Bounce", + "year": "2000", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DD+5.1", + "release_group": "VLAD", + "container": "mkv" + } + }, + { + "title": "Changing.Lanes.2002.1080p.BluRay.x264-CiNEFiLE.mkv", + "metadata": { + "cn_name": "", + "en_name": "Changing Lanes", + "year": "2002", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "CiNEFiLE", + "container": "mkv" + } + }, + { + "title": "The.Prestige.2006.1080p.BluRay.x264-WLM.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Prestige", + "year": "2006", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "WLM", + "container": "mkv" + } + }, + { + "title": "Monster.Hunter.2020.UHD.BluRay.2160p.x265.DV.HDR.TrueHD.Atmos.7.1.mUHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Monster Hunter", + "year": "2020", + "resolution": "2160p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "TrueHD.Atmos.7.1", + "release_group": "mUHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Dazed.and.Confused.1993.1080p.BluRay.REMUX.HYBRID.AVC-DTS-HD.MA.5.1-PmP.mkv", + "metadata": { + "cn_name": "", + "en_name": "Dazed and Confused", + "year": "1993", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "PmP", + "container": "mkv" + } + }, + { + "title": "Justice.League.2017.BluRay.1080p.TrueHD.Atmos.7.1.AVC.REMUX-FraMeSToR.mkv", + "metadata": { + "cn_name": "", + "en_name": "Justice League", + "year": "2017", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "TrueHD.Atmos.7.1", + "release_group": "FraMeSToR", + "container": "mkv" + } + }, + { + "title": "Suicide.Squad.2016.Extended.Cut.BluRay.1080p.TrueHD.Atmos.7.1.AVC.REMUX-FraMeSToR.mkv", + "metadata": { + "cn_name": "", + "en_name": "Suicide Squad", + "year": "2016", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "TrueHD.Atmos.7.1", + "release_group": "FraMeSToR", + "container": "mkv" + } + }, + { + "title": "The.Sum.of.All.Fears.2002.1080p.BluRay.DTS.x264-CyTSuNee.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Sum of All Fears", + "year": "2002", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS", + "release_group": "CyTSuNee", + "container": "mkv" + } + }, + { + "title": "The.Town.2010.EXTENDED.ALTERNATE.CUT.BluRay.1080p.DTSHD-MA.h264.Remux-decibeL.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Town", + "year": "2010", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTSHD-MA", + "release_group": "decibeL", + "container": "mkv" + } + }, + { + "title": "triple.frontier.2019.internal.multi.1080p.hdr.web.h265-carapils.mkv", + "metadata": { + "cn_name": "", + "en_name": "triple frontier", + "year": "2019", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "carapils", + "container": "mkv" + } + }, + { + "title": "Contratiempo.2016.1080p.ESP.BluRay.1080p.x265.10bit.DDP.5.1.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Contratiempo", + "year": "2016", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Terminator.2.Judgment.Day.1991.EXTENDED.REMASTERED.1080p.Blu-ray.x265.10bit.DTS-HD£cXcY@FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Terminator 2 Judgment Day", + "year": "1991", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS-HD", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "Malena.2000.UNCUT.BluRay.1080p.x265.10bit.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Malena", + "year": "2000", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "The.Bourne.Identity.2002.1080p.Blu-ray.x265.10bit.DTS£cXcY@FRDS(1.58.17).mkv", + "metadata": { + "cn_name": "", + "en_name": "The Bourne Identity", + "year": "2002", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DTS", + "release_group": "cXcY@FRDS", + "container": "mkv" + } + }, + { + "title": "Sseo-ni.AKA.Sunny.2011.Director's.Cut.1080p.KOR.Bluray.x265.10bit.DDP5.1.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Sseo ni", + "year": "2011", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "DDP.5.1", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "In.the.Heat.of.the.Sun.1994.1080p.WEB-DL.1080p.x265.10bit.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "In the Heat of the Sun", + "year": "1994", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.265", + "audio_codec": "", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Waterloo.Bridge.1940.BluRay.1080p.x265.10bit.FLAC.H.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Waterloo Bridge", + "year": "1940", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "FLAC", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "A.Good.Woman.2004.1080p.AMZN.WEB-DL.DD+5.1.H.264-monkee.mkv", + "metadata": { + "cn_name": "", + "en_name": "A Good Woman", + "year": "2004", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "DD+5.1", + "release_group": "monkee", + "container": "mkv" + } + }, + { + "title": "A.Love.Song.for.Bobby.Long.2004.Repack.1080p.Blu-ray.Remux.AVC.DTS-HD.MA.5.1.KRaLiMaRKo.mkv", + "metadata": { + "cn_name": "", + "en_name": "A Love Song for Bobby Long", + "year": "2004", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "KRaLiMaRKo", + "container": "mkv" + } + }, + { + "title": "Black.Widow.2021.NORDiC.REMUX.1080p.BluRay.AVC.TrueHD.Atmos7.1-TWA.mkv", + "metadata": { + "cn_name": "", + "en_name": "Black Widow", + "year": "2021", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "TrueHD.Atmos7.1", + "release_group": "TWA", + "container": "mkv" + } + }, + { + "title": "Captain.America.Civil.War.2016.1080p.BluRay.DTS.x264-HDMaNiAcS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Captain America Civil War", + "year": "2016", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS", + "release_group": "HDMaNiAcS", + "container": "mkv" + } + }, + { + "title": "Captain.Marvel.2019.1080p.UHD.BluRay.DD.5.1.x264-LoRD.mkv", + "metadata": { + "cn_name": "", + "en_name": "Captain Marvel", + "year": "2019", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DD.5.1", + "release_group": "LoRD", + "container": "mkv" + } + }, + { + "title": "Chef.2014.1080p.BluRay.DTS.x264-CyTSuNee.mkv", + "metadata": { + "cn_name": "", + "en_name": "Chef", + "year": "2014", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS", + "release_group": "CyTSuNee", + "container": "mkv" + } + }, + { + "title": "Eight.Legged.Freaks.2002.NORDiC.REMUX.1080p.BluRay.AVC.DTS-HD.MA5.1-TWA.mkv", + "metadata": { + "cn_name": "", + "en_name": "Eight Legged Freaks", + "year": "2002", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "TWA", + "container": "mkv" + } + }, + { + "title": "Ghost.in.the.Shell.2017.NORDiC.1080p.BluRay.x264.DTS-HD.MA.7.1-TWA.mkv", + "metadata": { + "cn_name": "", + "en_name": "Ghost in the Shell", + "year": "2017", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.7.1", + "release_group": "TWA", + "container": "mkv" + } + }, + { + "title": "Ghost.World.2001.Criterion.Collection.REMUX.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-LEGi0N.mkv", + "metadata": { + "cn_name": "", + "en_name": "Ghost World", + "year": "2001", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "LEGi0N", + "container": "mkv" + } + }, + { + "title": "Girl.with.a.Pearl.Earring.2003.1080p.PROPER.BluRay.x264-MOOVEE.mkv", + "metadata": { + "cn_name": "", + "en_name": "Girl with a Pearl Earring", + "year": "2003", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "MOOVEE", + "container": "mkv" + } + }, + { + "title": "Her.2013.1080p.BluRay.ReMuX.AVC.DTS-HD.MA.5.1.mkv", + "metadata": { + "cn_name": "", + "en_name": "Her", + "year": "2013", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA.5.1", + "release_group": "", + "container": "mkv" + } + }, + { + "title": "Hitchcock.2012.1080p.BluRay.x264-SECTOR7-4P.mkv", + "metadata": { + "cn_name": "", + "en_name": "Hitchcock", + "year": "2012", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "SECTOR7-4P", + "container": "mkv" + } + }, + { + "title": "Home.Alone.3.1997.1080p.WEB.x264-iNTENSO.mkv", + "metadata": { + "cn_name": "", + "en_name": "Home Alone 3", + "year": "1997", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "iNTENSO", + "container": "mkv" + } + }, + { + "title": "Iron.Man.2.2010.1080p.BluRay.REMUX.AVC.DTS-HD.MA_5.1-AROMA.mkv", + "metadata": { + "cn_name": "", + "en_name": "Iron Man 2", + "year": "2010", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "DTS-HD.MA_5.1", + "release_group": "AROMA", + "container": "mkv" + } + }, + { + "title": "Just.Cause.1995.1080p.Bluray.x264-HD4U.mkv", + "metadata": { + "cn_name": "", + "en_name": "Just Cause", + "year": "1995", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "HD4U", + "container": "mkv" + } + }, + { + "title": "Manny.and.Lo.1996.1080p.WEBRip.x264-RARBG.mp4", + "metadata": { + "cn_name": "", + "en_name": "Manny and Lo", + "year": "1996", + "resolution": "1080p", + "source": "WEBRip", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "RARBG", + "container": "mp4" + } + }, + { + "title": "Sing.2.2021.MULTI.1080p.WEB.H264-Slay3R.mkv", + "metadata": { + "cn_name": "", + "en_name": "Sing 2", + "year": "2021", + "resolution": "1080p", + "source": "WEB-DL", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "Slay3R", + "container": "mkv" + } + }, + { + "title": "The.Avengers.2012.Hybrid.1080p.BluRay.REMUX.AVC.Atmos-EPSiLON.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Avengers", + "year": "2012", + "resolution": "1080p", + "source": "BluRay.Remux", + "video_codec": "H.264", + "audio_codec": "Atmos", + "release_group": "EPSiLON", + "container": "mkv" + } + }, + { + "title": "The.Man.Who.Wasnt.There.2001.MULTi.1080p.BluRay.x264-FHD.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Man Who Wasnt There", + "year": "2001", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "FHD", + "container": "mkv" + } + }, + { + "title": "The.Prestige.2006.1080p.BluRay.x264-WLM.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Prestige", + "year": "2006", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "WLM", + "container": "mkv" + } + }, + { + "title": "changing.lanes.1080p.bluray.x264.sample-cinefile.mkv", + "metadata": { + "cn_name": "", + "en_name": "changing lanes", + "year": "", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "", + "release_group": "cinefile", + "container": "mkv" + } + }, + { + "title": "Jay and Silent Bob Strike Back 2001 1080p BluRay DTS x264-LEGi0N-Sample.mkv", + "metadata": { + "cn_name": "", + "en_name": "Jay and Silent Bob Strike Back", + "year": "2001", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.264", + "audio_codec": "DTS", + "release_group": "LEGi0N-Sample", + "container": "mkv" + } + }, + { + "title": "Ashes.of.Time.Redux.2008.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Ashes of Time", + "year": "2008", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audio", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Titanic.1997.Open.Matte.BluRay.1080p.x265.10bit.4Audio.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Titanic", + "year": "1997", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "4Audio", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "The.Martian.2015.Extended.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "The Martian", + "year": "2015", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audio", + "release_group": "MNHD-FRDS", + "container": "mkv" + } + }, + { + "title": "Farewell.My.Concubine.1993.BFI.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", + "metadata": { + "cn_name": "", + "en_name": "Farewell My Concubine", + "year": "1993", + "resolution": "1080p", + "source": "BluRay", + "video_codec": "H.265", + "audio_codec": "2Audio", + "release_group": "MNHD-FRDS", + "container": "mkv" + } } ] } \ No newline at end of file diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/107929 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/107929 new file mode 100644 index 00000000..e489dd18 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/107929 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/112 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/112 new file mode 100644 index 00000000..a81192e5 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/112 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/128 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/128 new file mode 100644 index 00000000..2f7e7953 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/128 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/144 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/144 new file mode 100644 index 00000000..57bd0ae9 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/144 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/160 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/160 new file mode 100644 index 00000000..eb214e97 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/160 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/176 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/176 new file mode 100644 index 00000000..f8f76943 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/176 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/192 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/192 new file mode 100644 index 00000000..8534a567 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/192 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/208 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/208 new file mode 100644 index 00000000..19625978 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/208 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/96 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/96 new file mode 100644 index 00000000..9908ce06 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/blobs/96 differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/conf b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/conf new file mode 100644 index 00000000..4154d7c4 --- /dev/null +++ b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/conf @@ -0,0 +1,4 @@ +segment_size: 524288 +use_compression: false +version: 0.34 +vQ \ No newline at end of file diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/db b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/db new file mode 100644 index 00000000..e0af061b Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/db differ diff --git a/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/snap.000000000001A5A9 b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/snap.000000000001A5A9 new file mode 100644 index 00000000..7d59bd73 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/fanart_http_cache/snap.000000000001A5A9 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/1373156 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/1373156 new file mode 100644 index 00000000..c88a06a3 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/1373156 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/1546510 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/1546510 new file mode 100644 index 00000000..9b4b0b33 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/1546510 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2198776 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2198776 new file mode 100644 index 00000000..ebdc415f Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2198776 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2282015 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2282015 new file mode 100644 index 00000000..e2cc026b Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2282015 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2294097 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2294097 new file mode 100644 index 00000000..332bc75e Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2294097 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2299645 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2299645 new file mode 100644 index 00000000..a325704d Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2299645 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2299661 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2299661 new file mode 100644 index 00000000..6c03a2bd Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/2299661 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/3366691 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/3366691 new file mode 100644 index 00000000..ddb433b5 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/blobs/3366691 differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/conf b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/conf new file mode 100644 index 00000000..4154d7c4 --- /dev/null +++ b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/conf @@ -0,0 +1,4 @@ +segment_size: 524288 +use_compression: false +version: 0.34 +vQ \ No newline at end of file diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/db b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/db new file mode 100644 index 00000000..b36655cb Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/db differ diff --git a/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/snap.000000000033CDB6 b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/snap.000000000033CDB6 new file mode 100644 index 00000000..399b89b5 Binary files /dev/null and b/soda_resource_tools_lib/tests/test_cache/imdb_http_cache/snap.000000000033CDB6 differ diff --git a/soda_resource_tools_lib/tests/test_scrape_mt_hardlink_src/.gitignore b/soda_resource_tools_lib/tests/test_scrape_mt_hardlink_src/.gitignore deleted file mode 100644 index 730ce4ba..00000000 --- a/soda_resource_tools_lib/tests/test_scrape_mt_hardlink_src/.gitignore +++ /dev/null @@ -1 +0,0 @@ -凡人修仙传.The.Mortal.Ascention \ No newline at end of file diff --git "a/soda_resource_tools_lib/tests/test_scrape_mt_hardlink_src/src/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H264.AAC-OurTV/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4" "b/soda_resource_tools_lib/tests/test_scrape_mt_hardlink_src/src/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H264.AAC-OurTV/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4" deleted file mode 100644 index e69de29b..00000000 diff --git "a/soda_resource_tools_lib/tests/test_scrape_mt_hardlink_src_to_target/src/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H264.AAC-OurTV/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4" "b/soda_resource_tools_lib/tests/test_scrape_mt_hardlink_src_to_target/src/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H264.AAC-OurTV/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4" deleted file mode 100644 index e69de29b..00000000 diff --git a/soda_resource_tools_lib/tests/test_scrape_mt_move_src/.gitignore b/soda_resource_tools_lib/tests/test_scrape_mt_move_src/.gitignore deleted file mode 100644 index 730ce4ba..00000000 --- a/soda_resource_tools_lib/tests/test_scrape_mt_move_src/.gitignore +++ /dev/null @@ -1 +0,0 @@ -凡人修仙传.The.Mortal.Ascention \ No newline at end of file diff --git "a/soda_resource_tools_lib/tests/test_scrape_mt_move_src/src/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H264.AAC-OurTV/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4" "b/soda_resource_tools_lib/tests/test_scrape_mt_move_src/src/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01.2160p.WEB-DL.H264.AAC-OurTV/\345\207\241\344\272\272\344\277\256\344\273\231\344\274\240.The.Mortal.Ascention.2020.S01E01.2160p.WEB-DL.H264.AAC-OurTV.mp4" deleted file mode 100644 index e69de29b..00000000