diff --git a/README.md b/README.md index 8c9187e..b957dbc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ A Python-based CLI tool for managing personal video collections with a safety-fi - **Metadata Enrichment**: Add bilingual titles and reputation signals (TMDB + optional AI fallback) - **Incremental Performance**: SQLite-backed cache avoids repeated metadata lookups - **State Tracking**: Track file status throughout the workflow -- **Detailed Reporting**: Generate inventory, completeness, and duplicate reports +- **Detailed Reporting**: Generate inventory, completeness, and duplicate reports (reports can include plan content summary via `--plan`) +- **Plan–Analysis Integration**: `vlm plan --analysis` applies duplicate resolution (keep by reputation, quarantine rest) and adds a Chinese human summary to the plan for quick review ## Installation @@ -120,9 +121,21 @@ Create a reviewable plan of file operations: vlm plan ``` -This creates `plan.json` with proposed operations (move, rename, quarantine). +To let the plan automatically resolve duplicate groups (keep one file per group by reputation, quarantine the rest), pass the analysis file: -**Review the plan** by opening `plan.json` in your editor. You can edit it if needed. +```bash +vlm plan --analysis analysis.json +``` + +This creates `plan.json` with: +- Proposed operations (move, rename, quarantine, no-op) +- **Summary**: counts by operation type and by reason +- **Human summary** (中文): short narrative for quick review +- **Metadata**: when using `--analysis`, duplicate groups considered and completeness gaps + +Duplicate keep strategy is configurable in `~/.vlm/config.yaml` under `plan.duplicate_keep` (`by_reputation`, `first_seen`, or `manual`). Default is `by_reputation` (prefer external rating; fallback to first-seen). + +**Review the plan** by opening `plan.json` in your editor, or read the human summary when you run `vlm execute`. You can edit the plan JSON if needed. ### 7. Execute (Dry-Run First) @@ -132,7 +145,7 @@ Preview what will happen without making changes: vlm execute ``` -When ready to actually move/rename files: +Before running, the CLI prints the plan’s **human summary** (or a short summary from counts) so you can confirm at a glance. When ready to actually move/rename/quarantine files: ```bash vlm execute --confirm @@ -175,9 +188,9 @@ vlm enrich vlm analyze # Output: analysis.json with 5 series with gaps, 12 duplicate groups -# 6. Generate execution plan -vlm plan -# Output: plan.json with 456 operations proposed +# 6. Generate execution plan (optionally use analysis for duplicate handling) +vlm plan --analysis analysis.json +# Output: plan.json with operations, human summary, and duplicate quarantine decisions # 7. Review the plan cat plan.json | less @@ -271,8 +284,12 @@ vlm analyze --input my_identities.json --output my_analysis.json # Generate plan with default files vlm plan -# Generate plan with custom files +# Use analysis so duplicate groups become "keep one + quarantine rest" (by_reputation by default) +vlm plan --analysis analysis.json + +# Custom input/output vlm plan --input my_identities.json --output my_plan.json +vlm plan --input my_identities.json --analysis my_analysis.json --output my_plan.json ``` ### Execution @@ -327,8 +344,12 @@ vlm report inventory --format json --output inventory_report.json # Generate completeness report (series with gaps) vlm report completeness +# Include plan content summary in the report (human_summary from plan.json) +vlm report completeness --plan plan.json + # Generate duplicates report vlm report duplicates +vlm report duplicates --plan plan.json # Generate summary statistics vlm report summary @@ -383,6 +404,11 @@ quarantine_dir: ".quarantine" # Logging level (DEBUG, INFO, WARNING, ERROR) log_level: "INFO" +# Plan behavior (e.g. when using vlm plan --analysis) +plan: + # Duplicate keep strategy: "by_reputation" (default), "first_seen", or "manual" + duplicate_keep: "by_reputation" + # Category mappings (directory name to category) categories: movie: [movie, movies, films] @@ -532,8 +558,9 @@ vlm enrich # 2. Analyze completeness vlm analyze -# 3. View report +# 3. View report (optionally include plan summary if you have a plan) vlm report completeness +vlm report completeness --plan plan.json ``` ### Scenario 3: Finding and Removing Duplicates @@ -550,12 +577,18 @@ vlm analyze # 3. View duplicates with quality comparison vlm report duplicates -# 4. Manually quarantine lower quality files +# 4. Generate plan with analysis: VLM keeps one file per duplicate group (by reputation) and quarantines the rest +vlm plan --analysis analysis.json + +# 5. Review plan (human summary in plan.json and when you run execute) +vlm execute +vlm execute --confirm + +# Alternatively: manual quarantine without plan vlm quarantine add /path/to/lower/quality/file.mkv --reason "duplicate - lower quality" -# 5. Or generate plan and let VLM suggest operations -vlm plan -vlm execute +# Report with plan context +vlm report duplicates --plan plan.json ``` ### Scenario 4: Reorganizing Your Library @@ -726,10 +759,11 @@ src/vlm/ ├── providers/ # External metadata providers (TMDB, etc.) │ ├── base.py # Provider interface │ └── tmdb.py # TMDB API client -├── io.py # JSON/CSV load/save and plan/analysis input helpers +├── io.py # JSON/CSV load/save, load_analysis_json, plan/analysis input helpers ├── utils.py # UTC time, format_size, etc. ├── analysis.py # Completeness and duplicate detection -├── planner.py # Execution plan generation +├── duplicate_resolve.py # Duplicate group keep-index (by_reputation, first_seen, manual) +├── planner.py # Execution plan generation (optionally consumes analysis) ├── executor.py # File operations and rollback ├── quarantine.py # Quarantine management ├── state.py # File state tracking diff --git a/analysis.json b/analysis.json index fdbbee2..18c587e 100644 --- a/analysis.json +++ b/analysis.json @@ -1,6 +1,6 @@ { "metadata": { - "generated": "2026-02-10T08:49:31", + "generated": "2026-02-10T09:54:17", "source_identities": "identities.json", "total_movies": 637, "total_series": 1492 diff --git a/plan.json b/plan.json index a30c8a9..dbe2c09 100644 --- a/plan.json +++ b/plan.json @@ -1,6 +1,6 @@ { - "plan_id": "853518f4-40ea-409f-8b5f-ab5d79908554", - "created_at": "2026-02-10T08:51:59.456050+00:00", + "plan_id": "7e4827a4-3692-4ebe-87c7-b92950c87ea9", + "created_at": "2026-02-10T09:54:19.304610+00:00", "operations": [ { "operation_type": "no-op", @@ -27,10 +27,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Your.Eyes.Tell.2020.720p.BluRay.x264-WiKi/Your.Eyes.Tell.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -43,10 +43,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Wish.2020.1080p.BluRay.x264.DTS-WiKi/Wish.2020.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -59,10 +59,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Amsterdam.2022.1080p.BluRay.x265.10bit.DTS-WiKi/Amsterdam.2022.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -83,10 +83,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Food.Luck.2020.720p.BluRay.x264-WiKi/Food.Luck.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -99,10 +99,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Next.Door.Neighbor.2020.720p.BluRay.x264-WiKi/Next.Door.Neighbor.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -131,10 +131,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/R.I.P.D.2013.1080p.BluRay.x264.DTS-WiKi/R.I.P.D.2013.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -427,10 +427,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Kingdom.2.Far.and.Away.2022.1080p.BluRay.x265.10bit-WiKi/Sample/Kingdom.2.Far.and.Away.2022.1080p.BluRay.x265.10bit-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -443,10 +443,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Exit.2019.720p.BluRay.x264-WiKi/Exit.2019.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -467,10 +467,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Fallen.1998.1080p.BluRay.x264.DTS-WiKi/Sample/Fallen.1998.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -483,10 +483,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Knives.Out.2019.1080p.BluRay.x264-WiKi/Sample/Knives.Out.2019.1080p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -499,10 +499,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Kaguya-sama.Love.Is.War.2019.1080p.BluRay.x264.DTS-WiKi/Sample/Kaguya-sama.Love.Is.War.2019.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -523,10 +523,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Happy.End.1999.1080p.BluRay.x265.10bit.DTS-WiKi/Happy.End.1999.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -539,10 +539,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Signal.The.Movie.2021.1080p.BluRay.x264.DTS-WiKi/Signal.The.Movie.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -555,10 +555,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/1778.Stories.of.Me.and.My.Wife.2011.1080p.BluRay.x264-WiKi/1778.Stories.of.Me.and.My.Wife.2011.1080p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -579,10 +579,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/AI.Amok.2020.720p.BluRay.x264-WiKi/AI.Amok.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -611,10 +611,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Ambulance.2022.1080p.BluRay.x265.10bit-WiKi/Ambulance.2022.1080p.BluRay.x265.10bit-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -675,10 +675,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Wings.of.the.Kirin.2012.1080p.BluRay.x264.DTS-WiKi/Sample/The.Wings.of.the.Kirin.2012.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -691,10 +691,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Flowers.2010.1080p.BluRay.x264-WiKi/Flowers.2010.1080p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -707,10 +707,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Ticket.to.Paradise.2022.1080p.BluRay.x264.DTS-WiKi/Sample/Ticket.to.Paradise.2022.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -739,10 +739,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Shock.Wave.2.2020.720p.BluRay.x264-WiKi/Sample/Shock.Wave.2.2020.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -755,10 +755,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.First.Gentleman.2021.720p.BluRay.x264-WiKi/The.First.Gentleman.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -779,10 +779,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/One.Summer.Story.2020.720p.BluRay.x264.DTS-WiKi/One.Summer.Story.2020.720p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -795,10 +795,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Card.Counter.2021.720p.BluRay.x264-WiKi/The.Card.Counter.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -835,10 +835,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Mauritanian.2021.1080p.BluRay.x264.DTS-WiKi/The.Mauritanian.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -875,10 +875,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Spy.2015.720p.BluRay.x264.DTS-WiKi/Spy.2015.720p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -891,10 +891,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Verdens.Verste.Menneske.2021.720p.BluRay.x264-WiKi/Sample/Verdens.Verste.Menneske.2021.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -907,10 +907,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Yowamushi.Pedal.2020.1080p.BluRay.x264.DTS-WiKi/Yowamushi.Pedal.2020.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -923,10 +923,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Sex.Is.Zero.2002.1080p.BluRay.x264.DTS-WiKi/Sex.Is.Zero.2002.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -939,10 +939,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Hayabusa.2011.1080p.BluRay.x264.DTS-WiKi/Hayabusa.2011.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -955,18 +955,18 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Top.Gun.Maverick.2022.1080p.BluRay.x265.10bit-WiKi/Sample/Top.Gun.Maverick.2022.1080p.BluRay.x265.10bit-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Top.Gun.Maverick.2022.1080p.BluRay.x265.10bit-WiKi/Top.Gun.Maverick.2022.1080p.BluRay.x265.10bit-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -979,10 +979,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Hating.Game.2021.1080p.BluRay.x264.DTS-WiKi/The.Hating.Game.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1011,10 +1011,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Hound.of.the.Baskervilles.Sherlock.the.Movie.2022.1080p.BluRay.x265.10bit.DTS-WiKi/The.Hound.of.the.Baskervilles.Sherlock.the.Movie.2022.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1027,10 +1027,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Lock.Stock.and.Two.Smoking.Barrels.1998.BluRay.1080p.x264.DTS-WiKi/Lock.Stock.and.Two.Smoking.Barrels.1998.BluRay.1080p.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1083,10 +1083,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/灿烂人生.The.Best.of.Youth.2003.Blu-ray.1080p.x265.10bit.DD5.1.MNHD-FRDS/The.Best.of.Youth.2003.Blu-ray.1080p.DISC1.x265.10bit.DD5.1.MNHD-FRDS.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1107,10 +1107,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Under.The.Tuscan.Sun.2003.1080p.BluRay.x264.DTS-WiKi/Under.The.Tuscan.Sun.2003.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1123,10 +1123,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Weathering.with.You.2019.1080p.BluRay.x264.DTS-WiKi/Weathering.with.You.2019.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1171,10 +1171,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Airport.2013.1080p.BluRay.x264-WiKi/Airport.2013.1080p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1195,10 +1195,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Lady.Chatterley's.Lover.1981.720p.BluRay.x264-WiKi/Lady.Chatterley's.Lover.1981.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1227,10 +1227,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Big.Shot's.Funeral.2001.720p.BluRay.x264-WiKi/Big.Shot's.Funeral.2001.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1267,10 +1267,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Freaky.2020.720p.BluRay.x264-WiKi/Freaky.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1291,10 +1291,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Father.2020.1080p.BluRay.x264.DTS-WiKi/The.Father.2020.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1307,10 +1307,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/No.Longer.Human.2019.720p.BluRay.x264-WiKi/Sample/No.Longer.Human.2019.720p.BluRay.x264-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1331,10 +1331,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Assassination.2015.1080p.BluRay.x264.DTS-WiKi/Assassination.2015.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1355,10 +1355,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Top.Gun.Maverick.2022.2160p.WEB-DL.DDP5.1.Atmos.HDR.HEVC-CMRG/Top.Gun.Maverick.2022.2160p.WEB-DL.DDP5.1.Atmos.HDR.HEVC-CMRG.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1379,10 +1379,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Basic.Instinct.Director's.Cut.1992.BluRay.720p.x264.DTS-WiKi/Sample/Basic.Instinct.Director's.Cut.1992.BluRay.720p.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1403,10 +1403,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Raya.and.the.Last.Dragon.2021.720p.BluRay.x264-WiKi/Sample/Raya.and.the.Last.Dragon.2021.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1435,10 +1435,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/I.Wish.2011.1080p.BluRay.x265.10bit-WiKi/I.Wish.2011.1080p.BluRay.x265.10bit-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1499,10 +1499,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Howl's.Moving.Castle.2004.1080p.BluRay.x264.DTS-WiKi/Howl's.Moving.Castle.2004.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1531,10 +1531,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Berserk.The.Golden.Age.Arc.The.Egg.of.the.King.2012.1080p.BluRay.x264.DTS-WiKi/Sample/Berserk.The.Golden.Age.Arc.The.Egg.of.the.King.2012.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1555,10 +1555,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Where.the.Crawdads.Sing.2022.1080p.BluRay.x265.10bit.DTS-WiKi/Where.the.Crawdads.Sing.2022.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1571,10 +1571,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/A.Loving.Husband.2016.1080p.BluRay.x264-WiKi/A.Loving.Husband.2016.1080p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1587,10 +1587,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/And.the.Baton.Was.Passed.2021.1080p.BluRay.x264-WiKi/And.the.Baton.Was.Passed.2021.1080p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1611,10 +1611,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Voice.of.Sin.2020.720p.BluRay.x264-WiKi/The.Voice.of.Sin.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1635,10 +1635,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Tazza.The.Hidden.Card.2014.720p.BluRay.x264-WiKi/Tazza.The.Hidden.Card.2014.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1659,10 +1659,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Baragaki.Unbroken.Samurai.2021.1080p.BluRay.x265.10bit.DTS-WiKi/Baragaki.Unbroken.Samurai.2021.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1691,10 +1691,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Be.My.Master.2018.1080p.BluRay.x264.DTS-WiKi/Be.My.Master.2018.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1707,10 +1707,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Serendipity.2001.1080p.BluRay.x264.DTS-WiKi/Sample/Serendipity.2001.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1763,10 +1763,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Thermae.Romae.2012.1080p.BluRay.x264.DTS-WiKi/Thermae.Romae.2012.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1819,10 +1819,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Summer.Sway.2020.1080p.BluRay.x264.DTS-WiKi/Summer.Sway.2020.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1835,26 +1835,26 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/战争与和平.War.and.Peace.1966.CC.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS/War.and.Peace.1966.Part.II.Natasha.Rostova.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/战争与和平.War.and.Peace.1966.CC.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS/War.and.Peace.1966.Part.IV.Pierre.Bezukhov.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/战争与和平.War.and.Peace.1966.CC.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS/War.and.Peace.1966.Part.I.Andrei.Bolkonsky.BluRay.1080p.x265.10bit.2Audio.MNHD-FRDS.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1883,10 +1883,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Encanto.2021.720p.BluRay.x264-WiKi/Sample/Encanto.2021.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1915,10 +1915,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Poupelle.of.Chimney.Town.2020.720p.BluRay.x264-WiKi/Poupelle.of.Chimney.Town.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1931,10 +1931,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Tenet.2020.IMAX.1080p.BluRay.x264.DTS-WiKi/Sample/Tenet.2020.IMAX.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1947,10 +1947,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Under.the.Open.Sky.2020.720p.BluRay.x264-WiKi/Under.the.Open.Sky.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -1979,10 +1979,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Wrath.of.Man.2021.720p.BluRay.x264-WiKi/Wrath.of.Man.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2003,10 +2003,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Intouchables.2011.720p.BluRay.x264.DTS-WiKi/Intouchables.2011.720p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2027,10 +2027,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Extreme.Job.2019.1080p.BluRay.x264.DTS-WiKi/Extreme.Job.2019.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2051,10 +2051,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Pass.Last.Days.of.the.Samurai.2020.1080p.BluRay.x265.10bit.DTS-WiKi/Sample/The.Pass.Last.Days.of.the.Samurai.2020.1080p.BluRay.x265.10bit.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2067,10 +2067,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Boss.Level.2020.1080p.BluRay.x264.DTS-WiKi/Sample/Boss.Level.2020.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2091,10 +2091,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Outpost.2020.720p.BluRay.x264-WiKi/Sample/The.Outpost.2020.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2123,10 +2123,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Summer.Wars.2009.1080p.BluRay.x264.DTS-WiKi/Summer.Wars.2009.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2147,10 +2147,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Crimes.That.Bind.2018.1080p.BluRay.x264.DTS-WiKi/The.Crimes.That.Bind.2018.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2171,10 +2171,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Bound.1996.1080p.1080p.BluRay.x264.DTS-WiKi/Sample/Bound.1996.1080p.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2211,10 +2211,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Spider-Man.No.Way.Home.2021.720p.BluRay.x264-WiKi/Sample/Spider-Man.No.Way.Home.2021.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2227,10 +2227,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Destiny.The.Tale.of.Kamakura.2017.1080p.BluRay.x264.DTS-WiKi/Sample/Destiny.The.Tale.of.Kamakura.2017.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2243,10 +2243,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/A.Christmas.Gift.from.Bob.2020.720p.BluRay.x264-WiKi/A.Christmas.Gift.from.Bob.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2307,10 +2307,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Girls.to.Buy.2021.Uncut.1080p.BluRay.x265.10bit.DTS-WiKi/Sample/Girls.to.Buy.2021.Uncut.1080p.BluRay.x265.10bit.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2347,10 +2347,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Pig.2021.720p.BluRay.x264-WiKi/Pig.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2371,18 +2371,18 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Your.Name.2016.RERiP.1080p.BluRay.x264.DTS-WiKi/Your.Name.2016.RERiP.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Inside.Out.2.2024.PROPER.UHD.BluRay.2160p.10bit.DV.3Audio.TrueHD(Atmos).7.1.x265-beAst/Inside.Out.2.2024.PROPER.UHD.BluRay.2160p.10bit.DV.3Audio.TrueHD(Atmos).7.1.x265-beAst.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2411,10 +2411,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Kakegurui.2.Ultimate.Russian.Roulette.2021.720p.BluRay.x264-WiKi/Kakegurui.2.Ultimate.Russian.Roulette.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2435,10 +2435,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/A.Moment.of.Romance.1990.1080p.BluRay.x264.DTS-WiKi/Sample/A.Moment.of.Romance.1990.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2451,10 +2451,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Golden.Slumber.2010.1080p.BluRay.x265.10bit.DTS-WiKi/Golden.Slumber.2010.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2507,10 +2507,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/A.Writer's.Odyssey.2021.720p.BluRay.x264-WiKi/A.Writer's.Odyssey.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2523,10 +2523,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Baby.Driver.2017.1080p.BluRay.x264.DTS-WiKi/Baby.Driver.2017.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2539,10 +2539,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Hard.Hit.2021.720p.BluRay.x264-WiKi/Sample/Hard.Hit.2021.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2603,10 +2603,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Let.Him.Go.2020.720p.BluRay.x264-WiKi/Let.Him.Go.2020.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2651,10 +2651,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Inside.Men.2015.Director's.Cut.720p.BluRay.x264-WiKi/Sample/Inside.Men.2015.Director's.Cut.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2675,10 +2675,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Fable.The Killer.Who.Doesn't.Kill.2021.1080p.BluRay.x264.DTS-WiKi/The.Fable.The Killer.Who.Doesn't.Kill.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2691,10 +2691,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Bad.Boys.for.Life.2020.1080p.BluRay.x264-WUTANG/Sample/bad.boys.for.life.2020.1080p.bluray.x264-wutang-sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2707,26 +2707,26 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Confidential.Assignment.2017.1080p.BluRay.x264-WiKi/Sample/Confidential.Assignment.2017.1080p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Weathering.with.You.2019.RERiP.1080p.BluRay.x264.DTS-WiKi/Weathering.with.You.2019.RERiP.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Weathering.with.You.2019.RERiP.1080p.BluRay.x264.DTS-WiKi/Sample/Weathering.with.You.2019.RERiP.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2747,10 +2747,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Muzi.v.nadeji.2011.1080p.BluRay.x264.DTS-WiKi/Muzi.v.nadeji.2011.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2795,10 +2795,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Maigret.2016.2in1.720p.BluRay.x264.DTS-WiKi/Maigret.Sets.a.Trap.2016.720p.BluRay.x264.DTS-WiKi/Sample/Maigret.Sets.a.Trap.2016.720p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2811,10 +2811,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Maigret.2016.2in1.720p.BluRay.x264.DTS-WiKi/Maigrets.Dead.Man.2016.720p.BluRay.x264.DTS-WiKi/Maigrets.Dead.Man.2016.720p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2827,10 +2827,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Shall.we.dance.1996.Remastered.1080p.BluRay.x265.10bit.DTS-WiKi/Shall.we.dance.1996.Remastered.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2867,10 +2867,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Way.Down.2021.720p.BluRay.x264-WiKi/Way.Down.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2883,10 +2883,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Parallel.World.Love.Story.2019.1080p.BluRay.x264.DTS-WiKi/Parallel.World.Love.Story.2019.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2907,10 +2907,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Door.Into.Summer.2021.720p.BluRay.x264-WiKi/The.Door.Into.Summer.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2923,10 +2923,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Confidence.Man.JP.SP.2019.720p.BluRay.x264-WiKi/The.Confidence.Man.JP.SP.2019.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2955,10 +2955,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Luca.2021.1080p.BluRay.x264.DTS-WiKi/Luca.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -2979,10 +2979,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Dog.2022.1080p.BluRay.x264.DTS-WiKi/Sample/Dog.2022.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3035,10 +3035,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Moonlight.Express.1999.720p.BluRay.x264-WiKi/Sample/Moonlight.Express.1999.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3075,10 +3075,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Lost.City.2022.1080p.BluRay.x265.10bit-WiKi/The.Lost.City.2022.1080p.BluRay.x265.10bit-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3099,10 +3099,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Shoplifters.2018.1080p.BluRay.x264.DTS-WiKi/Sample/Shoplifters.2018.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3155,10 +3155,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Devils.Advocate.1997.UNRATED.DC.1080p.BluRay.x264.DTS-WiKi/The.Devils.Advocate.1997.UNRATED.DC.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3187,10 +3187,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Fortune.Favors.Lady.Nikuko.2021.1080p.BluRay.x264.DTS-WiKi/Sample/Fortune.Favors.Lady.Nikuko.2021.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3211,10 +3211,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Wotakoi.Love.Is.Hard.for.Otaku.2020.720p.BluRay.x264.DTS-WiKi/Sample/Wotakoi.Love.Is.Hard.for.Otaku.2020.720p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3243,18 +3243,18 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Bullet.Train.2022.1080p.BluRay.x264.DTS-WiKi/Sample/Bullet.Train.2022.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Bullet.Train.2022.1080p.BluRay.x264.DTS-WiKi/Bullet.Train.2022.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3275,10 +3275,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Mole.Song.Final.2021.1080p.BluRay.x265.10bit.DTS-WiKi/The.Mole.Song.Final.2021.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3291,10 +3291,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Drive.My.Car.2021.1080p.BluRay.x264.DTS-WiKi/Drive.My.Car.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3331,10 +3331,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Ghost.Book.2022.1080p.BluRay.x265.10bit.DTS-WiKi/Ghost.Book.2022.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3387,10 +3387,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Deliver.Us.from.Evil.2020.720p.BluRay.x264-WiKi/Sample/Deliver.Us.from.Evil.2020.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3411,10 +3411,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Demolition.2015.720p.BluRay.x264-WiKi/Sample/Demolition.2015.720p.BluRay.x264-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3443,18 +3443,18 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Crayon.Shinchan.The.Tornado.Legend.of.Ninja.Mononoke.2022.1080p.BluRay.x265.10bit-WiKi/Sample/Crayon.Shinchan.The.Tornado.Legend.of.Ninja.Mononoke.2022.1080p.BluRay.x265.10bit-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Shin.Ultraman.2022.1080p.BluRay.x265.10bit.DTS-WiKi/Shin.Ultraman.2022.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3491,10 +3491,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Portrait.of.a.Beauty.2008.720p.BluRay.x264-WiKi/Sample/Portrait.of.a.Beauty.2008.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3507,10 +3507,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Made.in.Abyss.Journey's.Dawn.2019.720p.BluRay.x264-WiKi/Sample/Made.in.Abyss.Journey's.Dawn.2019.720p.BluRay.x264-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3555,10 +3555,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Unbearable.Weight.of.Massive.Talent.2022.1080p.BluRay.x265.10bit-WiKi/The.Unbearable.Weight.of.Massive.Talent.2022.1080p.BluRay.x265.10bit-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3571,10 +3571,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Tonkatsu.DJ.Agetaro.2020.720p.BluRay.x264.DTS-WiKi/Tonkatsu.DJ.Agetaro.2020.720p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3595,10 +3595,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Ghostbusters.Afterlife.2021.1080p.BluRay.x264.DTS-WiKi/Sample/Ghostbusters.Afterlife.2021.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3643,10 +3643,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Honest.Thief.2020.720p.BluRay.x264-WiKi/Sample/Honest.Thief.2020.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3667,10 +3667,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Master.Plan.2021.720p.BluRay.x264-WiKi/Sample/The.Master.Plan.2021.720p.BluRay.x264-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3691,10 +3691,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Northman.2022.RERiP.1080p.BluRay.x264-WiKi/Sample/The.Northman.2022.RERiP.1080p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3731,10 +3731,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Innocent.Witness.2019.720p.BluRay.x264-WiKi/Sample/Innocent.Witness.2019.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3747,10 +3747,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Fallen.Angels.1995.1080p.BluRay.x264.DTS-WiKi/Sample/Fallen.Angels.1995.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3771,10 +3771,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Cell.2000.720p.BluRay.x264.DTS-WiKi/Sample/The.Cell.2000.720p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3811,10 +3811,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Killer.2022.1080p.BluRay.x264.DTS-WiKi/Sample/The.Killer.2022.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3835,10 +3835,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Tron.Ares.2025.Hybrid.1080p.BluRay.DDP7.1.x264-ZoroSenpai.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3859,10 +3859,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Untold.Tale.of.the.Three.Kingdoms.2020.1080p.BluRay.x264.DTS-WiKi/Sample/The.Untold.Tale.of.the.Three.Kingdoms.2020.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3883,10 +3883,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Little.Things.2021.720p.BluRay.x264-WiKi/The.Little.Things.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3915,10 +3915,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/99.9.Criminal.Lawyer.The.Movie.2021.1080p.BluRay.x265.10bit.DTS-WiKi/99.9.Criminal.Lawyer.The.Movie.2021.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3947,10 +3947,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Sausalito.2000.720p.BluRay.x264-WiKi/Sausalito.2000.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3979,10 +3979,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/108.Reveng.and.Adventure.of.Goro.Kaiba.2019.720p.BluRay.x264-WiKi/Sample/108.Revenge.and.Adventure.of.Goro.Kaiba.2019.720p.BluRay.x264-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -3995,10 +3995,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Ura.Aka.L'Aventure.2021.1080p.BluRay.x264.DTS-WiKi/Ura.Aka.L'Aventure.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4011,10 +4011,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Soul.2020.1080p.BluRay.x264.DTS-WiKi/Sample/Soul.2020.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4059,10 +4059,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Godzilla.x.Kong.The.New.Empire.2024.2160p.WEB-DL.DDP5.1.HDR10+.H.265-POKE/Godzilla.x.Kong.The.New.Empire.2024.HDR.2160P.WEB.H265-POKE.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4115,10 +4115,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Wu.Kong.2017.1080p.BluRay.x264-WiKi/Sample/Wu.Kong.2017.1080p.BluRay.x264-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4139,10 +4139,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Program.2015.1080p.BluRay.x264.DTS-WiKi/Sample/The.Program.2015.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4155,10 +4155,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/McFarland.USA.2015.1080p.BluRay.x264.DTS-WiKi/McFarland.USA.2015.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4179,10 +4179,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Free.Guy.2021.1080p.BluRay.x264.DTS-WiKi/Free.Guy.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4195,10 +4195,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Obsessed.2014.1080p.BluRay.x265.10bit.DTS-WiKi/Obsessed.2014.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4235,10 +4235,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/In.the.Wake.2021.1080p.BluRay.x264.DTS-WiKi/Sample/In.the.Wake.2021.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4259,10 +4259,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Hitman's.Wife's.Bodyguard.2021.720p.BluRay.x264-WiKi/The.Hitman's.Wife's.Bodyguard.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4275,10 +4275,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Midnight.in.Paris.2011.1080p.BluRay.x264.DTS-WiKi/Sample/Midnight.in.Paris.2011.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4299,10 +4299,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Boite.noire.2021.1080p.BluRay.x265.10bit.DTS-WiKi/Boite.noire.2021.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4315,10 +4315,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Be.with.You.2004.1080p.BluRay.x264.DTS-WiKi/Sample/Be.with.You.2004.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4411,10 +4411,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Step.2020.720p.BluRay.x264-WiKi/Sample/Step.2020.720p.BluRay.x264-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4435,10 +4435,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Gura.Gura.2019.1080p.BluRay.x264.DTS-WiKi/Sample/Gura.Gura.2019.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4467,10 +4467,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Hunt.2020.1080p.BluRay.x264.DTS-WiKi/Sample/The.Hunt.2020.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4499,26 +4499,26 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/My.Missing.Valentine.2020.720p.BluRay.x264-WiKi/Sample/My.Missing.Valentine.2020.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Northman.2022.1080p.BluRay.x265.10bit-WiKi/Sample/The.Northman.2022.1080p.BluRay.x265.10bit-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Northman.2022.1080p.BluRay.x265.10bit-WiKi/The.Northman.2022.1080p.BluRay.x265.10bit-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4531,10 +4531,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Dirty.Grandpa.2016.1080p.BluRay.x264.DTS-WiKi/Dirty.Grandpa.2016.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4555,10 +4555,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/My.Blueberry.Nights.2007.BluRay.720p.x264.DTS-WiKi/My.Blueberry.Nights.2007.BluRay.720p.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4587,10 +4587,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Berserk.The.Golden.Age.Arc.2.The.Battle.for.Doldrey.2012.1080p.BluRay.x264.DTS-WiKi/Berserk.The.Golden.Age.Arc.2.The.Battle.for.Doldrey.2012.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4643,10 +4643,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Kim.Ji-young.Born.1982.2019.1080p.BluRay.x264.DTS-WiKi/Sample/Kim.Ji-young.Born.1982.2019.1080p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4675,10 +4675,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Anime.Supremacy.2022.1080p.BluRay.x265.10bit-WiKi/Sample/Anime.Supremacy.2022.1080p.BluRay.x265.10bit-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4707,10 +4707,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Confidence.Man.JP.Hero.2022.1080p.BluRay.x265.10bit.DTS-WiKi/The.Confidence.Man.JP.Hero.2022.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4755,10 +4755,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Attorney.2021.720p.BluRay.x264-WiKi/The.Attorney.2021.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4771,18 +4771,18 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Detective.vs.Sleuths.2022.1080p.BluRay.x265.10bit-WiKi/Sample/Detective.vs.Sleuths.2022.1080p.BluRay.x265.10bit-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Detective.vs.Sleuths.2022.1080p.BluRay.x265.10bit-WiKi/Detective.vs.Sleuths.2022.1080p.BluRay.x265.10bit-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4819,10 +4819,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/School.Meals.Time.Final.Battle.2020.720p.BluRay.x264-WiKi/Sample/School.Meals.Time.Final.Battle.2020.720p.BluRay.x264-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4835,10 +4835,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Fantastic.Beasts.The.Crimes.of.Grindelwald.2018.720p.BluRay.x264-WiKi/Fantastic.Beasts.The.Crimes.of.Grindelwald.2018.720p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4859,10 +4859,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/1917.2019.1080p.BluRay.x264-WiKi/1917.2019.1080p.BluRay.x264-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4875,10 +4875,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Crossing.Hennessy.2010.720p.BluRay.x264.DTS-WiKi/Sample/Crossing.Hennessy.2010.720p.BluRay.x264.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4899,18 +4899,18 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Sad.Movie.2005.720p.BluRay.x264-WiKi/Sample/Sad.Movie.2005.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/A.Man.2022.1080p.U-NEXT.WEB-DL.AAC2.0.H.264-xiaopie@CHDWEB.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4931,10 +4931,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Masquerade.Night.2021.1080p.BluRay.x264.DTS-WiKi/Masquerade.Night.2021.1080p.BluRay.x264.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4947,10 +4947,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Druk.2020.720p.BluRay.x264-WiKi/Sample/Druk.2020.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4963,10 +4963,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Bad.Guys.The.Movie.2019.720p.BluRay.x264-WiKi/Sample/Bad.Guys.The.Movie.2019.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4979,10 +4979,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Chickenhare.and.the.Hamster.of.Darkness.2022.1080p.BluRay.x265.10bit.DTS-WiKi/Sample/Chickenhare.and.the.Hamster.of.Darkness.2022.1080p.BluRay.x265.10bit.DTS-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -4995,10 +4995,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Thermae.Romae.II.2014.1080p.BluRay.x264.DTS-WiKi/Sample/Thermae.Romae.II.2014.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -5011,10 +5011,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Chilli.Laugh.Story.2022.1080p.BluRay.x265.10bit-WiKi/Sample/Chilli.Laugh.Story.2022.1080p.BluRay.x265.10bit-WiKi.Sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -5027,10 +5027,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/Top.Gun.Maverick.2022.IMAX.2160p.BluRay.x265.10bit.Atmos.TrueHD7.1-WiKi/Top.Gun.Maverick.2022.IMAX.2160p.BluRay.x265.10bit.Atmos.TrueHD7.1-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -5051,10 +5051,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Confidence.Man.JP.Princess.2020.1080p.BluRay.x264.DTS-WiKi/Sample/The.Confidence.Man.JP.Princess.2020.1080p.BluRay.x264.DTS-WiKi.sample.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -5067,10 +5067,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/movies/The.Contractor.2022.1080p.BluRay.x265.10bit.DTS-WiKi/The.Contractor.2022.1080p.BluRay.x265.10bit.DTS-WiKi.mkv", "destination_path": null, - "reason": "Movie needs manual review (no year found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -6563,10 +6563,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/series/Unsere.Muetter.unsere.Vaeter.S01.720p.BluRay.x264-WiKi/Sample/Unsere.Muetter.unsere.Vaeter.S01E03.720p.BluRay.x264-WiKi.Sample.mkv", "destination_path": null, - "reason": "Series needs manual review (no season/episode found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -8859,10 +8859,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/series/When.Life.Gives.You.Tangerines.S01.2025.1080p.NF.WEB-DL.x264.DDP5.1.Atmos-ADWeb/When.Life.Gives.You.Tangerines.S01E12.2025.1080p.NF.WEB-DL.x264.DDP5.1.Atmos-ADWeb.mkv", "destination_path": null, - "reason": "Series needs manual review (no season/episode found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -14499,10 +14499,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/series/Shogun.S01E07.2024.2160p.DSNP.WEB-DL.DDP5.1.DV.HDR.H.265-HHWEB.mkv", "destination_path": null, - "reason": "Series needs manual review (no season/episode found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -15851,10 +15851,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/series/Shogun.S01E08.2024.2160p.DSNP.WEB-DL.H265.HDR.DDP5.1.mkv", "destination_path": null, - "reason": "Series needs manual review (no season/episode found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -16331,10 +16331,10 @@ "conflict_reason": null }, { - "operation_type": "no-op", + "operation_type": "quarantine", "source_path": "/mnt/Downloads/series/【高清剧集网发布 www.TTHDTT.com】未知的首尔[第08集][中文字幕].Our.Unwritten.Seoul.S01.1080p.NF.WEB-DL.AAC.2.0.H.264-BlackTV/Our.Unwritten.Seoul.S01E08.An.Unconventional.Whole.1080p.NF.WEB-DL.AAC.2.0.H.264-BlackTV.mkv", "destination_path": null, - "reason": "Series needs manual review (no season/episode found)", + "reason": "重复项(已按外部评分保留一条)", "has_conflict": false, "conflict_reason": null }, @@ -37359,7 +37359,20 @@ "total": 4669, "move": 0, "rename": 0, - "quarantine": 0, - "no-op": 4669 + "quarantine": 187, + "no-op": 4482 + }, + "summary_by_reason": { + "Movie needs manual review (no year found)": 455, + "重复项(已按外部评分保留一条)": 187, + "Series needs manual review (no season/episode found)": 1487, + "Anime files not organized in v1": 2503, + "Other files not organized in v1": 37 + }, + "human_summary": "本计划共 4669 条操作:move 0,rename 0,quarantine 187,no-op 4482。\n原因分布:\n - Anime files not organized in v1: 2503 条\n - Series needs manual review (no season/episode found): 1487 条\n - Movie needs manual review (no year found): 455 条\n - 重复项(已按外部评分保留一条): 187 条\n - Other files not organized in v1: 37 条\n依据 analysis:重复组 183 个;剧集缺口 2 季。", + "metadata": { + "analysis_source": "identities.json", + "duplicate_groups_considered": 183, + "completeness_seasons_with_gaps": 2 } } \ No newline at end of file diff --git a/src/vlm/cli.py b/src/vlm/cli.py index 60f15c0..ecef5ee 100644 --- a/src/vlm/cli.py +++ b/src/vlm/cli.py @@ -600,8 +600,14 @@ def analyze(ctx: CLIContext, input: Path, output: Path, inventory: Optional[Path default=Path('plan.json'), help='Path to save execution plan (default: plan.json)' ) +@click.option( + '--analysis', + type=click.Path(path_type=Path), + default=None, + help='Path to analysis JSON (optional); when provided, duplicate groups are applied to the plan' +) @pass_context -def plan(ctx: CLIContext, input: Path, output: Path): +def plan(ctx: CLIContext, input: Path, output: Path, analysis: Path | None): """Generate execution plan. Creates a structured, reviewable plan of all file operations to be performed. @@ -611,11 +617,12 @@ def plan(ctx: CLIContext, input: Path, output: Path): vlm plan # Use default files vlm plan --input my_identities.json # Custom input + vlm plan --analysis analysis.json # Use analysis for duplicate handling vlm plan --output my_plan.json # Custom output """ try: from vlm.commands.plan import plan_cmd - plan_cmd(ctx, input, output) + plan_cmd(ctx, input, output, analysis) except FileNotFoundError: click.echo(f"Error: Input file not found: {input}", err=True) ctx.logger.error(f"Input file not found: {input}") @@ -678,6 +685,17 @@ def execute(ctx: CLIContext, plan: Path, confirm: bool): click.echo(f"Execution plan loaded: {execution_plan.plan_id}") click.echo(f"Created at: {execution_plan.created_at}") click.echo(f"Total operations: {len(execution_plan.operations)}") + if execution_plan.human_summary: + click.echo() + click.echo(execution_plan.human_summary) + elif execution_plan.summary or execution_plan.summary_by_reason: + s = execution_plan.summary or {} + by_r = execution_plan.summary_by_reason or {} + parts = [f"操作统计:共 {s.get('total', len(execution_plan.operations))} 条(move {s.get('move', 0)},rename {s.get('rename', 0)},quarantine {s.get('quarantine', 0)},no-op {s.get('no-op', 0)})"] + if by_r: + parts.append("原因分布:" + ";".join(f"{r}: {c}" for r, c in list(by_r.items())[:5])) + click.echo() + click.echo("\n".join(parts)) click.echo() # Display mode warning @@ -697,7 +715,7 @@ def execute(ctx: CLIContext, plan: Path, confirm: bool): click.echo() # Create execution engine and execute plan - engine = ExecutionEngine(logger=logger) + engine = ExecutionEngine(logger=logger, config=config) results, summary, rollback_log = engine.execute_plan( execution_plan, mode=mode, @@ -1036,7 +1054,7 @@ def rollback(ctx: CLIContext, log: Optional[Path]): click.echo() # Create execution engine - engine = ExecutionEngine(logger=logger) + engine = ExecutionEngine(logger=logger, config=config) # Load rollback log rollback_log = engine.load_rollback_log(log) @@ -1115,6 +1133,20 @@ def rollback(ctx: CLIContext, log: Optional[Path]): sys.exit(1) +def _fallback_plan_summary(execution_plan) -> str: + """Build a short plan summary from summary and summary_by_reason when human_summary is empty.""" + s = execution_plan.summary or {} + by_r = execution_plan.summary_by_reason or {} + total = s.get("total", len(execution_plan.operations)) + parts = [ + f"计划操作统计:共 {total} 条(move {s.get('move', 0)},rename {s.get('rename', 0)}," + f"quarantine {s.get('quarantine', 0)},no-op {s.get('no-op', 0)})" + ] + if by_r: + parts.append("原因分布:" + ";".join(f"{r}: {c}" for r, c in list(by_r.items())[:8])) + return "\n".join(parts) + + @main.group() @pass_context def report(ctx: CLIContext): @@ -1263,8 +1295,14 @@ def report_inventory(ctx: CLIContext, format: str, input: Path, output: Optional default=None, help='Output file (default: print to console)' ) +@click.option( + '--plan', + type=click.Path(path_type=Path), + default=None, + help='Optional plan JSON; when provided, report includes plan content summary' +) @pass_context -def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optional[Path]): +def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optional[Path], plan: Optional[Path]): """Generate completeness report. Shows series with episode gaps detected through heuristic analysis. @@ -1273,14 +1311,24 @@ def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optio vlm report completeness # Text format to console vlm report completeness --format json # JSON format to console + vlm report completeness --plan plan.json # Include plan content summary vlm report completeness --format text --output completeness.txt """ import json from vlm.reports import generate_completeness_report from vlm.models import SeasonCompleteness - + from vlm.planner import load_plan + config = ctx.config logger = ctx.logger + + plan_summary = None + if plan: + if not plan.exists(): + click.echo(f"Error: Plan file not found: {plan}", err=True) + sys.exit(1) + execution_plan = load_plan(plan) + plan_summary = execution_plan.human_summary or _fallback_plan_summary(execution_plan) try: # Load analysis from JSON @@ -1307,7 +1355,9 @@ def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optio # Generate report click.echo(f"Generating completeness report in {format} format...") - report_content = generate_completeness_report(season_completeness, format, config.library_root) + report_content = generate_completeness_report( + season_completeness, format, config.library_root, plan_summary=plan_summary + ) # Output report if output: @@ -1358,8 +1408,14 @@ def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optio default=None, help='Output file (default: print to console)' ) +@click.option( + '--plan', + type=click.Path(path_type=Path), + default=None, + help='Optional plan JSON; when provided, report includes plan content summary' +) @pass_context -def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optional[Path]): +def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optional[Path], plan: Optional[Path]): """Generate duplicate report. Shows duplicate files with quality comparison data to help decide which @@ -1369,15 +1425,25 @@ def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optiona vlm report duplicates # Text format to console vlm report duplicates --format json # JSON format to console + vlm report duplicates --plan plan.json # Include plan content summary vlm report duplicates --format text --output duplicates.txt """ import json from vlm.reports import generate_duplicate_report from vlm.models import DuplicateGroup, MovieIdentity, SeriesIdentity, VideoFile + from vlm.planner import load_plan from datetime import datetime, timezone config = ctx.config logger = ctx.logger + + plan_summary = None + if plan: + if not plan.exists(): + click.echo(f"Error: Plan file not found: {plan}", err=True) + sys.exit(1) + execution_plan = load_plan(plan) + plan_summary = execution_plan.human_summary or _fallback_plan_summary(execution_plan) try: # Load analysis from JSON @@ -1439,7 +1505,9 @@ def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optiona # Generate report click.echo(f"Generating duplicate report in {format} format...") - report_content = generate_duplicate_report(duplicate_groups, format, config.library_root) + report_content = generate_duplicate_report( + duplicate_groups, format, config.library_root, plan_summary=plan_summary + ) # Output report if output: diff --git a/src/vlm/commands/plan.py b/src/vlm/commands/plan.py index 459907c..6b7483d 100644 --- a/src/vlm/commands/plan.py +++ b/src/vlm/commands/plan.py @@ -1,20 +1,24 @@ """Plan command implementation.""" +import json from pathlib import Path +from typing import Optional import click from vlm.context import CLIContext -from vlm.io import identities_to_plan_input, load_identities_json +from vlm.io import identities_to_plan_input, load_identities_json, load_analysis_json from vlm.planner import generate_plan, save_plan -def plan_cmd(ctx: CLIContext, input: Path, output: Path) -> None: - """Generate execution plan from identities.""" +def plan_cmd(ctx: CLIContext, input: Path, output: Path, analysis: Optional[Path] = None) -> None: + """Generate execution plan from identities; optionally use analysis for duplicate handling.""" config = ctx.config logger = ctx.logger click.echo(f"Generating execution plan from: {input}") + if analysis: + click.echo(f"Using analysis: {analysis}") click.echo() identities_data = load_identities_json(input) @@ -31,8 +35,21 @@ def plan_cmd(ctx: CLIContext, input: Path, output: Path) -> None: identities_list = identities_to_plan_input(identities_data) + analysis_data = None + if analysis: + if not analysis.exists(): + click.echo(f"Error: Analysis file not found: {analysis}", err=True) + click.echo("Run 'vlm analyze' first to generate analysis.json.", err=True) + raise FileNotFoundError(analysis) + try: + analysis_data = load_analysis_json(analysis) + except (json.JSONDecodeError, KeyError) as e: + click.echo(f"Error: Invalid or incomplete analysis file: {e}", err=True) + click.echo("Run 'vlm analyze' to regenerate analysis.json.", err=True) + raise + click.echo("Generating execution plan...") - execution_plan = generate_plan(identities_list, config) + execution_plan = generate_plan(identities_list, config, analysis_data=analysis_data) click.echo() click.echo("Plan generation complete!") diff --git a/src/vlm/config.py b/src/vlm/config.py index 68df8c2..5a89e82 100644 --- a/src/vlm/config.py +++ b/src/vlm/config.py @@ -47,6 +47,9 @@ class Config: reputation_policy: str = "flag_for_review" naming_title_format: str = "{title_zh} {title_en}" + # Plan settings (e.g. duplicate handling when consuming analysis) + duplicate_keep: str = "by_reputation" + def load_config(path: Path) -> Config: """Load configuration from YAML file.""" @@ -86,6 +89,9 @@ def load_config(path: Path) -> Config: "anime": ["anime"] }) + plan = data.get("plan", {}) + duplicate_keep = plan.get("duplicate_keep", "by_reputation") + enrichment = data.get("enrichment") if enrichment is None: enrichment = data.get("enrich", {}) @@ -126,6 +132,7 @@ def load_config(path: Path) -> Config: reputation_low_score_threshold=reputation.get("low_score_threshold", 6.0), reputation_policy=reputation.get("policy", "flag_for_review"), naming_title_format=naming.get("title_format", "{title_zh} {title_en}"), + duplicate_keep=duplicate_keep, ) @@ -168,9 +175,12 @@ def create_default_config(path: Path) -> Config: }, } + plan_content = {"duplicate_keep": default_config.duplicate_keep} + yaml_content = { "library_root": str(default_config.library_root), "video_extensions": default_config.video_extensions, + "plan": plan_content, "templates": { "movie_dir": default_config.movie_template, "series_dir": default_config.series_template, @@ -314,5 +324,9 @@ def validate_config(config: Config) -> list[str]: errors.append("tmdb_region must be a string when set") if not isinstance(config.tmdb_include_adult, bool): errors.append("tmdb_include_adult must be a boolean") + if config.duplicate_keep not in ("by_reputation", "first_seen", "manual"): + errors.append( + f"duplicate_keep must be one of 'by_reputation', 'first_seen', 'manual', got: {config.duplicate_keep!r}" + ) return errors diff --git a/src/vlm/duplicate_resolve.py b/src/vlm/duplicate_resolve.py new file mode 100644 index 0000000..1ca8b7b --- /dev/null +++ b/src/vlm/duplicate_resolve.py @@ -0,0 +1,51 @@ +"""Duplicate group resolution: choose which file to keep when consuming analysis.""" + +from pathlib import Path +from typing import Union + +from vlm.models import MovieIdentity, SeriesIdentity + + +def choose_keep_index( + items: list[tuple[Path, Union[MovieIdentity, SeriesIdentity]]], + strategy: str, +) -> int | None: + """Choose the index of the item to keep in a duplicate group. + + Strategies: + - by_reputation: Prefer items with reputation_score; then sort by score desc, + then reputation_votes desc, then first_seen (input order). Keep index 0 after sort. + - first_seen: Keep the first item (index 0). + - manual: Return None; caller should not generate quarantine ops, only record in metadata. + + Args: + items: List of (path, identity) for the duplicate group. + strategy: One of "by_reputation", "first_seen", "manual". + + Returns: + Index in [0, len(items)) to keep, or None for manual. + """ + if strategy == "manual": + return None + if strategy == "first_seen": + return 0 + if strategy == "by_reputation": + return _by_reputation_index(items) + return 0 + + +def _by_reputation_index( + items: list[tuple[Path, Union[MovieIdentity, SeriesIdentity]]], +) -> int: + """Sort by: has reputation > no reputation; then score desc; then votes desc; then order. Return 0.""" + def key(idx_reason: tuple[int, tuple[Path, Union[MovieIdentity, SeriesIdentity]]]) -> tuple: + idx, (path, identity) = idx_reason + has_rep = identity.reputation_score is not None + score = identity.reputation_score if identity.reputation_score is not None else -1.0 + votes = identity.reputation_votes if identity.reputation_votes is not None else -1 + # Prefer has reputation (True > False), then higher score, then higher votes, then lower index + return (not has_rep, -score, -votes, idx) + + indexed = list(enumerate(items)) + indexed.sort(key=key) + return indexed[0][0] diff --git a/src/vlm/executor.py b/src/vlm/executor.py index d9619ed..c6b2773 100644 --- a/src/vlm/executor.py +++ b/src/vlm/executor.py @@ -14,21 +14,32 @@ from pathlib import Path from typing import Optional from uuid import uuid4 +from .config import Config from .logging_config import get_logger, log_operation from .models import ExecutionPlan, FileOperation, OperationResult, RollbackLog +from .quarantine import QuarantineManager from .utils import ensure_utc, utc_now class ExecutionEngine: """Engine for executing file operations safely with dry-run support.""" - - def __init__(self, logger: Optional[logging.Logger] = None): + + def __init__( + self, + logger: Optional[logging.Logger] = None, + config: Optional[Config] = None, + ): """Initialize the execution engine. - + Args: logger: Optional logger instance (uses default if not provided) + config: Optional config (required for quarantine operations) """ self.logger = logger or get_logger() + self.config = config + self._quarantine_manager: Optional[QuarantineManager] = ( + QuarantineManager(config, self.logger) if config else None + ) def execute_plan( self, @@ -134,7 +145,40 @@ class ExecutionEngine: error_message=None, executed_at=executed_at ) - + + # Handle quarantine operations (no destination_path; use QuarantineManager) + if operation.operation_type == "quarantine": + if not self._quarantine_manager: + return OperationResult( + operation=operation, + success=False, + error_message="Config required for quarantine operations", + executed_at=executed_at + ) + if mode == "dry-run": + log_operation( + self.logger, + logging.INFO, + f"[DRY-RUN] Would quarantine: {operation.source_path} ({operation.reason})", + operation_type="execute", + file_path=operation.source_path + ) + return OperationResult( + operation=operation, + success=True, + error_message=None, + executed_at=executed_at + ) + result = self._quarantine_manager.quarantine_file( + operation.source_path, operation.reason + ) + return OperationResult( + operation=operation, + success=result.success, + error_message=result.error_message, + executed_at=result.executed_at + ) + # Handle conflicted operations if operation.has_conflict: log_operation( @@ -171,11 +215,16 @@ class ExecutionEngine: Returns: OperationResult indicating what would happen """ + dest = operation.destination_path + msg = ( + f"[DRY-RUN] Would quarantine: {operation.source_path} ({operation.reason})" + if operation.operation_type == "quarantine" + else f"[DRY-RUN] Would {operation.operation_type}: {operation.source_path} -> {dest}" + ) log_operation( self.logger, logging.INFO, - f"[DRY-RUN] Would {operation.operation_type}: " - f"{operation.source_path} -> {operation.destination_path}", + msg, operation_type="execute", file_path=operation.source_path ) @@ -589,4 +638,3 @@ class ExecutionEngine: summary_msg, operation_type="rollback" ) - diff --git a/src/vlm/io.py b/src/vlm/io.py index fc38fdb..7aaa30e 100644 --- a/src/vlm/io.py +++ b/src/vlm/io.py @@ -17,11 +17,21 @@ __all__ = [ "save_inventory_csv", "load_identities_json", "save_identities_json", + "load_analysis_json", "identities_to_plan_input", "identities_to_analysis_input", ] +def load_analysis_json(path: Path) -> dict: + """Load analysis result from JSON file (metadata, completeness, duplicates). + + Caller should check file existence and handle missing/invalid keys. + """ + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + def load_identities_json(path: Path) -> dict: """Load identities from JSON file.""" with open(path, "r", encoding="utf-8") as f: diff --git a/src/vlm/models.py b/src/vlm/models.py index cc819ac..75eb3f6 100644 --- a/src/vlm/models.py +++ b/src/vlm/models.py @@ -128,17 +128,24 @@ class FileOperation: @dataclass class ExecutionPlan: """Represents a complete execution plan with all file operations. - + Attributes: plan_id: Unique identifier for the plan (UUID) created_at: Timestamp when the plan was created operations: List of file operations to execute summary: Dictionary with operation counts by type + summary_by_reason: Optional count per reason string (for human review) + human_summary: Optional short Chinese narrative summary of the plan + metadata: Optional dict (e.g. analysis_source, duplicate_groups_considered, + completeness_seasons_with_gaps) when plan was built from analysis """ plan_id: str created_at: datetime operations: list[FileOperation] summary: dict + summary_by_reason: dict = field(default_factory=dict) + human_summary: str = "" + metadata: dict = field(default_factory=dict) @dataclass diff --git a/src/vlm/planner.py b/src/vlm/planner.py index dea0521..02c8d76 100644 --- a/src/vlm/planner.py +++ b/src/vlm/planner.py @@ -8,9 +8,10 @@ import json import uuid from datetime import datetime from pathlib import Path -from typing import Union +from typing import Optional, Union from vlm.config import Config +from vlm.duplicate_resolve import choose_keep_index from vlm.utils import ensure_utc, utc_now from vlm.models import ( ExecutionPlan, @@ -20,38 +21,71 @@ from vlm.models import ( VideoFile, ) +QUARANTINE_REASON_DUPLICATE = "重复项(已按外部评分保留一条)" + def generate_plan( identities: list[tuple[VideoFile, Union[MovieIdentity, SeriesIdentity, None]]], - config: Config + config: Config, + analysis_data: Optional[dict] = None, ) -> ExecutionPlan: - """Generate an execution plan from parsed identities. - - Creates file operations for organizing video files based on their parsed - identities and configuration templates. Handles movies, series, anime, - and other categories according to v1 constraints. - - Args: - identities: List of tuples containing (VideoFile, parsed_identity) - config: Configuration with templates and settings - - Returns: - ExecutionPlan with all file operations and summary + """Generate an execution plan from parsed identities; optionally apply analysis duplicates. + + When analysis_data is provided and duplicate_keep is not "manual", duplicate groups + are resolved (one kept, rest quarantined) according to config.duplicate_keep. """ operations = [] - for video_file, identity in identities: operation = _create_operation(video_file, identity, config) operations.append(operation) - - # Generate summary counts + + metadata: dict = {} + if analysis_data is not None: + metadata["analysis_source"] = analysis_data.get("metadata", {}).get("source_identities", "") + metadata["duplicate_groups_considered"] = len(analysis_data.get("duplicates", [])) + metadata["completeness_seasons_with_gaps"] = len(analysis_data.get("completeness", [])) + + if config.duplicate_keep != "manual": + path_to_index = {str(vf.path): i for i, (vf, _) in enumerate(identities)} + for dup in analysis_data.get("duplicates", []): + paths = dup.get("files", []) + indices = [path_to_index[p] for p in paths if p in path_to_index] + items = [ + (identities[i][0].path, identities[i][1]) + for i in indices + if identities[i][1] is not None + and isinstance(identities[i][1], (MovieIdentity, SeriesIdentity)) + ] + if not items: + continue + keep_idx = choose_keep_index(items, config.duplicate_keep) + if keep_idx is None: + continue + keep_identity_index = indices[keep_idx] + quarantine_indices = set(indices) - {keep_identity_index} + for i in quarantine_indices: + vf = identities[i][0] + operations[i] = FileOperation( + operation_type="quarantine", + source_path=vf.path, + destination_path=None, + reason=QUARANTINE_REASON_DUPLICATE, + has_conflict=False, + conflict_reason=None, + ) + summary = _generate_summary(operations) - + summary_by_reason = _generate_summary_by_reason(operations) + human_summary = _generate_human_summary(operations, summary, summary_by_reason, metadata) + return ExecutionPlan( plan_id=str(uuid.uuid4()), created_at=utc_now(), operations=operations, - summary=summary + summary=summary, + summary_by_reason=summary_by_reason, + human_summary=human_summary, + metadata=metadata, ) @@ -322,10 +356,50 @@ def _generate_summary(operations: list[FileOperation]) -> dict: op_type = operation.operation_type if op_type in summary: summary[op_type] += 1 - + return summary +def _generate_summary_by_reason(operations: list[FileOperation]) -> dict[str, int]: + """Aggregate operation counts by reason string.""" + out: dict[str, int] = {} + for op in operations: + r = op.reason + out[r] = out.get(r, 0) + 1 + return out + + +def _generate_human_summary( + operations: list[FileOperation], + summary: dict, + summary_by_reason: dict[str, int], + metadata: dict, +) -> str: + """Build a short Chinese narrative summary of the plan.""" + total = summary.get("total", len(operations)) + move = summary.get("move", 0) + rename = summary.get("rename", 0) + quarantine = summary.get("quarantine", 0) + noop = summary.get("no-op", 0) + conflicts = sum(1 for op in operations if op.has_conflict) + + parts = [ + f"本计划共 {total} 条操作:move {move},rename {rename},quarantine {quarantine},no-op {noop}。" + ] + if summary_by_reason: + reason_lines = [f" - {r}: {c} 条" for r, c in sorted(summary_by_reason.items(), key=lambda x: -x[1])[:10]] + parts.append("原因分布:") + parts.extend(reason_lines) + if conflicts > 0: + parts.append(f"冲突 {conflicts} 条。") + if metadata: + dup = metadata.get("duplicate_groups_considered", 0) + gaps = metadata.get("completeness_seasons_with_gaps", 0) + if dup or gaps: + parts.append(f"依据 analysis:重复组 {dup} 个;剧集缺口 {gaps} 季。") + return "\n".join(parts) + + def save_plan(plan: ExecutionPlan, output_path: Path) -> None: """Save execution plan to JSON file. @@ -351,7 +425,10 @@ def save_plan(plan: ExecutionPlan, output_path: Path) -> None: } for op in plan.operations ], - "summary": plan.summary + "summary": plan.summary, + "summary_by_reason": plan.summary_by_reason, + "human_summary": plan.human_summary, + "metadata": plan.metadata, } # Write to JSON file with indentation for human readability @@ -398,5 +475,8 @@ def load_plan(input_path: Path) -> ExecutionPlan: plan_id=plan_dict["plan_id"], created_at=created_at, operations=operations, - summary=plan_dict["summary"] - ) + summary=plan_dict["summary"], + summary_by_reason=plan_dict.get("summary_by_reason", {}), + human_summary=plan_dict.get("human_summary", ""), + metadata=plan_dict.get("metadata", {}), + ) \ No newline at end of file diff --git a/src/vlm/quarantine.py b/src/vlm/quarantine.py index d158e38..ae875e0 100644 --- a/src/vlm/quarantine.py +++ b/src/vlm/quarantine.py @@ -510,6 +510,25 @@ class QuarantineManager: return entries + def find_quarantine_path_by_original(self, original_path: Path) -> Optional[Path]: + """Find the quarantine path for a file that was quarantined from original_path. + + Used when rolling back a quarantine operation when the rollback log only + has the original (source) path and not the actual quarantine destination. + + Args: + original_path: The original path of the file before quarantine + + Returns: + The path where the file was moved in quarantine, or None if not found + """ + for category in ("movie", "series"): + manifest = self._load_manifest(category) + for entry in manifest.entries: + if entry.original_path == original_path: + return entry.quarantine_path + return None + def restore_from_quarantine( self, quarantine_path: Path diff --git a/src/vlm/reports.py b/src/vlm/reports.py index 82181ab..bd43027 100644 --- a/src/vlm/reports.py +++ b/src/vlm/reports.py @@ -151,30 +151,42 @@ def _generate_inventory_json( def generate_completeness_report( analysis: list[SeasonCompleteness], format: str, - library_root: Path + library_root: Path, + plan_summary: str | None = None, ) -> str: """Generate completeness report showing series with episode gaps. - + Args: analysis: List of SeasonCompleteness objects with detected gaps format: Output format ("text" or "json") library_root: Root of the library (included in report metadata) - + plan_summary: Optional plan content summary to prepend (when --plan was used) + Returns: Formatted report as string - + Raises: ValueError: If format is not "text" or "json" """ if format not in ["text", "json"]: raise ValueError(f"Invalid format: {format}. Must be 'text' or 'json'") - + generation_timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") - + if format == "json": - return _generate_completeness_json(analysis, generation_timestamp, library_root) - else: # text - return _generate_completeness_text(analysis, generation_timestamp, library_root) + body = _generate_completeness_json(analysis, generation_timestamp, library_root) + else: + body = _generate_completeness_text(analysis, generation_timestamp, library_root) + + if plan_summary: + if format == "text": + section = "计划内容总结\n" + "-" * 40 + "\n" + plan_summary + "\n\n" + return section + body + else: + data = json.loads(body) + data["plan_summary"] = plan_summary + return json.dumps(data, indent=2, ensure_ascii=False) + return body def _generate_completeness_text( @@ -264,30 +276,42 @@ def _generate_completeness_json( def generate_duplicate_report( duplicates: list[DuplicateGroup], format: str, - library_root: Path + library_root: Path, + plan_summary: str | None = None, ) -> str: """Generate duplicate report showing duplicate files with quality comparisons. - + Args: duplicates: List of DuplicateGroup objects with duplicate files format: Output format ("text" or "json") library_root: Root of the library (included in report metadata) - + plan_summary: Optional plan content summary to prepend (when --plan was used) + Returns: Formatted report as string - + Raises: ValueError: If format is not "text" or "json" """ if format not in ["text", "json"]: raise ValueError(f"Invalid format: {format}. Must be 'text' or 'json'") - + generation_timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") - + if format == "json": - return _generate_duplicate_json(duplicates, generation_timestamp, library_root) - else: # text - return _generate_duplicate_text(duplicates, generation_timestamp, library_root) + body = _generate_duplicate_json(duplicates, generation_timestamp, library_root) + else: + body = _generate_duplicate_text(duplicates, generation_timestamp, library_root) + + if plan_summary: + if format == "text": + section = "计划内容总结\n" + "-" * 40 + "\n" + plan_summary + "\n\n" + return section + body + else: + data = json.loads(body) + data["plan_summary"] = plan_summary + return json.dumps(data, indent=2, ensure_ascii=False) + return body def _generate_duplicate_text( diff --git a/tests/test_executor.py b/tests/test_executor.py index 31c474d..4747dcc 100644 --- a/tests/test_executor.py +++ b/tests/test_executor.py @@ -10,6 +10,7 @@ from uuid import uuid4 import pytest +from vlm.config import Config from vlm.executor import ExecutionEngine from vlm.models import ExecutionPlan, FileOperation @@ -904,3 +905,43 @@ class TestRollbackExecution: actual_failed = sum(1 for r in rollback_results if not r.success) assert rollback_summary["successful"] == actual_success assert rollback_summary["failed"] == actual_failed + + def test_rollback_quarantine_operation(self, tmp_path): + """Test that rollback restores quarantined files via QuarantineManager.""" + library_root = tmp_path / "library" + library_root.mkdir() + (library_root / "movie").mkdir() + config = Config(library_root=library_root, quarantine_dir=".quarantine") + engine = ExecutionEngine(logger=logging.getLogger("test_executor"), config=config) + + original_path = library_root / "movie" / "Duplicate (2020).mkv" + original_path.write_text("movie content") + + plan = ExecutionPlan( + plan_id=str(uuid4()), + created_at=datetime.now(), + operations=[ + FileOperation( + operation_type="quarantine", + source_path=original_path, + destination_path=None, + reason="duplicate", + has_conflict=False, + conflict_reason=None, + ), + ], + summary={"quarantine": 1}, + ) + + results, _, rollback_log = engine.execute_plan(plan, mode="execute", confirmed=True) + assert results[0].success + assert not original_path.exists() + quarantine_path = config.library_root / "movie" / ".quarantine" / "Duplicate (2020).mkv" + assert quarantine_path.exists() + + rollback_results, rollback_summary = engine.rollback(rollback_log) + assert rollback_summary["successful"] == 1 + assert rollback_summary["failed"] == 0 + assert original_path.exists() + assert original_path.read_text() == "movie content" + assert not quarantine_path.exists()