diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2fcae05ccda..26e2df7c04b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,11 +6,6 @@ A few sentences describing the overall goals of the pull request's commits. #### Screenshot (if UI related) -#### Todos -- [ ] Tests -- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json) -- [ ] [Wiki Updates](https://wiki.servarr.com) - #### Issues Fixed or Closed by this PR -* Fixes #XXXX \ No newline at end of file +* Fixes #XXXX diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b79c618010c..f7f9c8d1653 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,7 @@ jobs: build_number: ${{ steps.build-meta.outputs.build_number }} radarr_version: ${{ steps.build-meta.outputs.radarr_version }} branch_name: ${{ steps.build-meta.outputs.branch_name }} + branch_name_safe: ${{ steps.build-meta.outputs.branch_name_safe }} backend_changed: ${{ steps.changes.outputs.backend_changed }} steps: - name: Checkout @@ -53,33 +54,10 @@ jobs: HEAD_REF: ${{ github.head_ref }} REF_NAME: ${{ github.ref_name }} EVENT_NAME: ${{ github.event_name }} - BASE_REF: ${{ github.base_ref }} + RUN_NUMBER: ${{ github.run_number }} run: | - git fetch --tags --force - - LAST=$(git tag -l 'build-counter-*' | sed 's/build-counter-//' | sort -n | tail -1) - BUILD_NUMBER=$(( ${LAST:-2999} + 1 )) - - if [ "$EVENT_NAME" != "pull_request" ]; then - tag_pushed=false - for attempt in 1 2 3; do - if git tag "build-counter-${BUILD_NUMBER}" && git push origin "build-counter-${BUILD_NUMBER}"; then - echo "Tag pushed successfully on attempt $attempt" - tag_pushed=true - break - fi - echo "Tag push failed on attempt $attempt, retrying..." - git tag -d "build-counter-${BUILD_NUMBER}" 2>/dev/null || true - git fetch --tags --force - LAST=$(git tag -l 'build-counter-*' | sed 's/build-counter-//' | sort -n | tail -1) - BUILD_NUMBER=$(( LAST + 1 )) - done - - if [ "$tag_pushed" != "true" ]; then - echo "::error::Failed to reserve a build counter tag after 3 attempts" - exit 1 - fi - fi + # Keep fork build numbers in the existing 30xx range without mutating repo state. + BUILD_NUMBER=$(( 3000 + RUN_NUMBER )) if [ "$EVENT_NAME" = "pull_request" ]; then BRANCH_NAME="$HEAD_REF" @@ -87,10 +65,12 @@ jobs: BRANCH_NAME="$REF_NAME" fi + BRANCH_NAME_SAFE=$(printf '%s' "$BRANCH_NAME" | sed 's#[^A-Za-z0-9._-]#-#g') { echo "build_number=$BUILD_NUMBER" echo "radarr_version=${MAJOR_VERSION}.${BUILD_NUMBER}" echo "branch_name=$BRANCH_NAME" + echo "branch_name_safe=$BRANCH_NAME_SAFE" } >> "$GITHUB_OUTPUT" - name: Detect backend changes @@ -225,7 +205,7 @@ jobs: env: RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} BUILD_SOURCEBRANCHNAME: ${{ needs.setup.outputs.branch_name }} - BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} + BUILDNAME: ${{ needs.setup.outputs.branch_name_safe }}.${{ needs.setup.outputs.radarr_version }} steps: - name: Checkout uses: actions/checkout@v4 @@ -389,7 +369,7 @@ jobs: packages: write env: RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} - BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} + BUILDNAME: ${{ needs.setup.outputs.branch_name_safe }}.${{ needs.setup.outputs.radarr_version }} REGISTRY: ghcr.io IMAGE_NAME: staros-labs/radarr strategy: @@ -583,30 +563,43 @@ jobs: runs-on: ubuntu-24.04 needs: [build-backend, setup] if: needs.setup.outputs.backend_changed == 'true' - container: - image: ghcr.io/servarr/testimages:alpine + env: + MUSL_TEST_IMAGE: ghcr.io/servarr/testimages:alpine timeout-minutes: 10 steps: - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Download test artifact uses: actions/download-artifact@v4 with: name: linux-musl-x64-tests path: _tests/ - - name: Set executable permissions + - name: Download dotnet installer run: | - chmod a+x _tests/ffprobe - find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + mkdir -p _temp + curl -fsSL https://dot.net/v1/dotnet-install.sh -o _temp/dotnet-install.sh + chmod a+x _temp/dotnet-install.sh - - name: Run unit tests + - name: Run unit tests in Alpine container run: | - chmod a+x _tests/test.sh - _tests/test.sh Linux Unit Test + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -e DOTNET_VERSION="${DOTNET_VERSION}" \ + -e HOME=/tmp/home \ + -e TEST_DIR=/work/_tests \ + -v "${PWD}:/work" \ + -w /work \ + "${MUSL_TEST_IMAGE}" \ + sh -lc ' + set -e + mkdir -p "$HOME" + export DOTNET_ROOT=/tmp/dotnet + export PATH="$DOTNET_ROOT:$PATH" + bash /work/_temp/dotnet-install.sh --version "$DOTNET_VERSION" --install-dir "$DOTNET_ROOT" --architecture x64 --os linux-musl --no-path + chmod a+x _tests/ffprobe + find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + chmod a+x _tests/test.sh + _tests/test.sh Linux Unit Test + ' - name: Upload test results uses: actions/upload-artifact@v4 @@ -768,15 +761,10 @@ jobs: runs-on: ubuntu-24.04 needs: [packages, build-backend, setup] if: needs.setup.outputs.backend_changed == 'true' - container: - image: ghcr.io/servarr/testimages:alpine + env: + MUSL_TEST_IMAGE: ghcr.io/servarr/testimages:alpine timeout-minutes: 15 steps: - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Download test artifact uses: actions/download-artifact@v4 with: @@ -797,15 +785,33 @@ jobs: mkdir -p ./bin/ cp -r _extracted/Radarr/. ./bin/ 2>/dev/null || cp -r _extracted/. ./bin/ - - name: Set executable permissions + - name: Download dotnet installer run: | - chmod a+x _tests/ffprobe - find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + mkdir -p _temp + curl -fsSL https://dot.net/v1/dotnet-install.sh -o _temp/dotnet-install.sh + chmod a+x _temp/dotnet-install.sh - - name: Run integration tests + - name: Run integration tests in Alpine container run: | - chmod a+x _tests/test.sh - _tests/test.sh Linux Integration Test + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -e DOTNET_VERSION="${DOTNET_VERSION}" \ + -e HOME=/tmp/home \ + -e TEST_DIR=/work/_tests \ + -v "${PWD}:/work" \ + -w /work \ + "${MUSL_TEST_IMAGE}" \ + sh -lc ' + set -e + mkdir -p "$HOME" + export DOTNET_ROOT=/tmp/dotnet + export PATH="$DOTNET_ROOT:$PATH" + bash /work/_temp/dotnet-install.sh --version "$DOTNET_VERSION" --install-dir "$DOTNET_ROOT" --architecture x64 --os linux-musl --no-path + chmod a+x _tests/ffprobe + find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + chmod a+x _tests/test.sh + _tests/test.sh Linux Integration Test + ' - name: Upload test results uses: actions/upload-artifact@v4 @@ -897,7 +903,7 @@ jobs: RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} MAJORVERSION: '6.1.1' BUILD_SOURCEBRANCHNAME: ${{ needs.setup.outputs.branch_name }} - BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} + BUILDNAME: ${{ needs.setup.outputs.branch_name_safe }}.${{ needs.setup.outputs.radarr_version }} steps: - name: Checkout uses: actions/checkout@v4 @@ -1074,13 +1080,15 @@ jobs: shell: pwsh - name: Generate coverage report - uses: danielpalme/ReportGenerator-GitHub-Action@5 if: always() - with: - reports: CoverageResults/**/coverage.cobertura.xml - targetdir: CoverageResults/combined - reporttypes: HtmlInline;Cobertura;Badges - sourcedirs: src + run: | + dotnet tool install --tool-path ./.tools dotnet-reportgenerator-globaltool --version 5.* + ./.tools/reportgenerator ` + "-reports:CoverageResults/**/coverage.cobertura.xml" ` + "-targetdir:CoverageResults/combined" ` + "-reporttypes:HtmlInline;Cobertura;Badges" ` + "-sourcedirs:src" + shell: pwsh # --------------------------------------------------------------------------- # API Docs: generate OpenAPI spec, create PR diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..f3c553a4f19 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# Radarr Fork Agent Instructions + +These instructions apply to automated builders and reviewers working in this repository. + +## Session Startup + +- Work from a git worktree, not directly in the main checkout. +- Read this file at the start of each session before making changes. +- Check `git status --short --branch` before editing. +- Assume the worktree may contain user changes. Do not revert or clean them up unless the user explicitly asks. + +## Communication + +- Keep updates concise and factual. +- Act like an involved collaborator, not a passive executor. +- Push back when a request is risky, underspecified, or likely to create avoidable maintenance problems. +- Use role-based language such as `builder` or `reviewer` if you need to describe automation. +- Do not use emojis in commits, pull requests, issues, comments, or docs. +- Do not mention assistant product names in commits, pull requests, comments, or repo files. + +## Branch Workflow + +- This fork uses `develop` as the integration branch and `master` as the production branch. +- Keep upstream-sync or vendor-tracking work separate from Staros-specific release work. +- Do not assume upstream defaults are the same as this fork's release process. + +## Editing Standards + +- Match the existing .NET, frontend, and GitHub Actions patterns already in the repo. +- Keep changes narrow and avoid unrelated cleanup in the same commit. +- Use `rg` for search and `apply_patch` for manual edits. +- Update docs or workflow guidance when branch behavior, release flow, or automation expectations change. + +## Validation + +- Run the smallest useful check for the files you changed. +- For workflow-only changes, use focused review plus `git diff --check`. +- If you cannot run a validation step, state that clearly and call out the remaining risk. +## Git worktrees + +Worktree directory preference: create git worktrees under `~/worktrees//`, never inside this clone or `~/Documents/Repos.nosync/`. Merged/clean worktrees are auto-pruned weekly by `infra/scripts/prune-worktrees.sh` (launchd `dev.staros.worktree-janitor`). diff --git a/build.sh b/build.sh index 47d97e13c09..8ce3a35e05b 100755 --- a/build.sh +++ b/build.sh @@ -15,13 +15,24 @@ ProgressEnd() echo "Finish '$1'" } +EscapeSedReplacement() +{ + printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g' +} + UpdateVersionNumber() { if [ "$RADARRVERSION" != "" ]; then + local escaped_version + local escaped_branch_name + + escaped_version=$(EscapeSedReplacement "$RADARRVERSION") + escaped_branch_name=$(EscapeSedReplacement "$BUILD_SOURCEBRANCHNAME") + echo "Updating Version Info" - sed -i'' -e "s/[0-9.*]\+<\/AssemblyVersion>/$RADARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props - sed -i'' -e "s/[\$()A-Za-z-]\+<\/AssemblyConfiguration>/${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props - sed -i'' -e "s/10.0.0.0<\/string>/$RADARRVERSION<\/string>/g" distribution/osx/Radarr.app/Contents/Info.plist + sed -i'' -e "s|[0-9.*]\+|${escaped_version}|g" src/Directory.Build.props + sed -i'' -e "s|[\$()A-Za-z/-]\+|${escaped_branch_name}|g" src/Directory.Build.props + sed -i'' -e "s|10.0.0.0|${escaped_version}|g" distribution/osx/Radarr.app/Contents/Info.plist fi } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.css b/frontend/src/Components/Page/Sidebar/PageSidebar.css index ee44c0407fe..68835532b86 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.css +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.css @@ -1,6 +1,7 @@ .sidebarContainer { flex: 0 0 $sidebarWidth; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; width: $sidebarWidth; background-color: var(--sidebarBackgroundColor); transition: transform 300ms ease-in-out; @@ -10,7 +11,8 @@ .sidebar { display: flex; flex-direction: column; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; background-color: var(--sidebarBackgroundColor); color: var(--white); } @@ -20,12 +22,15 @@ position: fixed; top: 0; z-index: 2; + overflow-x: hidden; + overflow-y: auto; height: 100vh; } .sidebar { position: fixed; z-index: 2; + overflow-x: hidden; overflow-y: auto; width: 100%; height: 100%; diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs index 2371d7fb573..9c13fac774c 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs @@ -18,9 +18,7 @@ public void Setup() } [TestCase("Prometheus", "Prometheus")] - - // TODO: TMDB Doesn't like when we clean periods from this - // [TestCase("The Man from U.N.C.L.E.", "The Man from U.N.C.L.E.")] + [TestCase("The Man from U.N.C.L.E.", "The Man from U.N.C.L.E.")] [TestCase("imdb:tt2527336", "Star Wars: The Last Jedi")] [TestCase("imdb:tt2798920", "Annihilation")] [TestCase("https://www.imdb.com/title/tt0033467/", "Citizen Kane")] diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyTitleNormalizationFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyTitleNormalizationFixture.cs new file mode 100644 index 00000000000..b53bfc40704 --- /dev/null +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyTitleNormalizationFixture.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MetadataSource.SkyHook; + +namespace NzbDrone.Core.Test.MetadataSource.SkyHook +{ + [TestFixture] + public class SkyHookProxyTitleNormalizationFixture + { + [TestCase("The Man from U.N.C.L.E.", "the man from u.n.c.l.e.")] + [TestCase("R.I.P.D.", "r.i.p.d.")] + public void should_preserve_meaningful_periods_when_normalizing_titles(string title, string expected) + { + SkyHookProxy.NormalizeSearchTitle(title).Should().Be(expected); + } + + [TestCase("the man from u.n.c.l.e.", "the+man+from+u.n.c.l.e.")] + [TestCase("r.i.p.d.", "r.i.p.d.")] + [TestCase("movie_title", "movie+title")] + public void should_only_replace_word_separators_when_building_search_terms(string title, string expected) + { + SkyHookProxy.BuildSearchTerm(title).Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/EditionParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/EditionParserFixture.cs index ecea2d5c385..a02aa1b316b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/EditionParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/EditionParserFixture.cs @@ -24,6 +24,7 @@ public class EditionParserFixture : CoreTest [TestCase("Movie 2012 IMAX.mkv", "IMAX")] [TestCase("Movie 2012 Restored.mkv", "Restored")] [TestCase("Movie Title.Special.Edition.Fan Edit.2012..BRRip.x264.AAC-m2g", "Special Edition Fan Edit")] + [TestCase("Mission.Impossible.3.2011.Special.Edition.1080p.BluRay.x264-SPARKS", "Special Edition")] [TestCase("Movie Title (Despecialized) 1999.mkv", "Despecialized")] [TestCase("Movie Title.(Special.Edition.Remastered).2012.[Bluray-1080p].mkv", "Special Edition Remastered")] [TestCase("Movie Title Extended 2012", "Extended")] diff --git a/src/NzbDrone.Core.Test/ParserTests/ExtendedQualityParserRegex.cs b/src/NzbDrone.Core.Test/ParserTests/ExtendedQualityParserRegex.cs index 88e3f6fc9b0..6d6bb8f5e81 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ExtendedQualityParserRegex.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ExtendedQualityParserRegex.cs @@ -22,11 +22,13 @@ public void Setup() [TestCase("Movie.Title.Super.Duper.Real.Proper.HDTV.x264-FTP", 0)] [TestCase("Movie.Title.PROPER.HDTV.x264-RiVER-RP", 0)] [TestCase("Movie.Title.PROPER.REAL.RERIP.1080p.BluRay.x264-TENEIGHTY", 1)] + [TestCase("Movie.Title.2013.REAL.PROPER.1080p.BluRay.x264-GRP", 1)] [TestCase("[MGS] - Movie.Title - Episode 02v2 - [D8B6C90D]", 0)] [TestCase("[Hatsuyuki] Movie Title - 07 [v2][848x480][23D8F455].avi", 0)] [TestCase("[DeadFish] Movie Title - 01v3 [720p][AAC]", 0)] [TestCase("[DeadFish] Movie Title Sword - 01v4 [720p][AAC]", 0)] [TestCase("The Real Movie.Titlewives of Some Place - S01E01 - Why are we doing this?", 0)] + [TestCase("The.Real.Movie.2013.1080p.BluRay.x264-GRP", 0)] public void should_parse_reality_from_title(string title, int reality) { QualityParser.ParseQuality(title).Revision.Real.Should().Be(reality); diff --git a/src/NzbDrone.Core.Test/ParserTests/Model/ReleaseInfoFixture.cs b/src/NzbDrone.Core.Test/ParserTests/Model/ReleaseInfoFixture.cs new file mode 100644 index 00000000000..c5b4c609958 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/Model/ReleaseInfoFixture.cs @@ -0,0 +1,68 @@ +using System; +using FluentAssertions; +using Newtonsoft.Json; +using NUnit.Framework; +using NzbDrone.Core.Parser.Model; +using STJsonSerializer = System.Text.Json.JsonSerializer; + +namespace NzbDrone.Core.Test.ParserTests.Model +{ + [TestFixture] + public class ReleaseInfoFixture + { + [Test] + public void should_calculate_age_values_from_publish_date() + { + var release = new ReleaseInfo + { + PublishDate = DateTime.UtcNow.AddMinutes(-90) + }; + + release.Age.Should().Be(0); + release.AgeHours.Should().BeApproximately(1.5, 0.05); + release.AgeMinutes.Should().BeApproximately(90, 1); + } + + [Test] + public void should_deserialize_newtonsoft_payloads_with_age_fields() + { + var publishDate = DateTime.UtcNow.AddHours(-2); + var json = $$""" + { + "PublishDate": "{{publishDate:O}}", + "Age": 999, + "AgeHours": 999, + "AgeMinutes": 999 + } + """; + + var release = JsonConvert.DeserializeObject(json); + + release.Should().NotBeNull(); + release.Age.Should().Be(0); + release.AgeHours.Should().BeApproximately(2, 0.05); + release.AgeMinutes.Should().BeApproximately(120, 1); + } + + [Test] + public void should_deserialize_system_text_json_payloads_with_age_fields() + { + var publishDate = DateTime.UtcNow.AddHours(-3); + var json = $$""" + { + "PublishDate": "{{publishDate:O}}", + "Age": 999, + "AgeHours": 999, + "AgeMinutes": 999 + } + """; + + var release = STJsonSerializer.Deserialize(json); + + release.Should().NotBeNull(); + release.Age.Should().Be(0); + release.AgeHours.Should().BeApproximately(3, 0.05); + release.AgeMinutes.Should().BeApproximately(180, 1); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index c226bdc53df..afdb4b95933 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -279,6 +279,33 @@ public void should_parse_hardcoded_subs(string postTitle, string sub) Parser.Parser.ParseMovieTitle(postTitle).HardcodedSubs.Should().Be(sub); } + [TestCase("The.Movie.from.U.N.C.L.E.2015.1080p.BluRay.x264-SPARKS", "SPARKS")] + [TestCase("R.I.P.D.2013.720p.BluRay.x264-SPARKS", "SPARKS")] + public void should_parse_release_group_for_titles_with_periods(string postTitle, string releaseGroup) + { + Parser.Parser.ParseMovieTitle(postTitle).ReleaseGroup.Should().Be(releaseGroup); + } + + [TestCase("The.Movie.from.U.N.C.L.E.2015.1080p.BluRay.x264-SPARKS", "A.Movie.2015.1080p.BluRay.x264-SPARKS")] + [TestCase("R.I.P.D.2013.Special.Edition.720p.BluRay.x264-SPARKS", "A.Movie.2013.Special.Edition.720p.BluRay.x264-SPARKS")] + public void should_build_simple_release_title_for_titles_with_periods(string postTitle, string simpleReleaseTitle) + { + Parser.Parser.ParseMovieTitle(postTitle).SimpleReleaseTitle.Should().Be(simpleReleaseTitle); + } + + [Test] + public void should_parse_edition_for_titles_with_periods_when_edition_follows_year() + { + var parsed = Parser.Parser.ParseMovieTitle("R.I.P.D.2013.Special.Edition.720p.BluRay.x264-SPARKS"); + + using (new AssertionScope()) + { + parsed.Edition.Should().Be("Special Edition"); + parsed.ReleaseGroup.Should().Be("SPARKS"); + parsed.SimpleReleaseTitle.Should().Be("A.Movie.2013.Special.Edition.720p.BluRay.x264-SPARKS"); + } + } + [TestCase("That Italian Movie 2008 [tt1234567] 720p BluRay X264", "tt1234567")] [TestCase("That Italian Movie 2008 [tt12345678] 720p BluRay X264", "tt12345678")] public void should_parse_imdb_in_title(string postTitle, string imdb) diff --git a/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs b/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs index 6a1ec22216a..90a2aeeffe3 100644 --- a/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs @@ -23,6 +23,8 @@ public class UrlFixture : CoreTest [TestCase("test123.ca - Movie Time 2023 720p HDTV x264 CRON", "Movie Time")] [TestCase("[www.test-hyphen123.co.za] - Movie Title 2023", "Movie Title")] [TestCase("(movieawake.com) Movie Title 2023 [720p] [English Subbed]", "Movie Title")] + [TestCase("[url]www.test.com[/url] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")] + [TestCase("[url=https://www.test.com]www.test.com[/url] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")] public void should_not_parse_url_in_name(string postTitle, string title) { var result = Parser.Parser.ParseMovieTitle(postTitle).MovieTitle.CleanMovieTitle(); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 6afb7326980..4794869cc9d 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -339,6 +339,16 @@ private string StripTrailingTheFromTitle(string title) return title; } + internal static string NormalizeSearchTitle(string title) + { + return title.ToLowerInvariant(); + } + + internal static string BuildSearchTerm(string title) + { + return title.Replace("_", "+").Replace(" ", "+"); + } + public MovieMetadata MapMovieToTmdbMovie(MovieMetadata movie) { try @@ -422,9 +432,7 @@ public List SearchForNewMovie(string title) } } - var lowerTitle = title.ToLower(); - - lowerTitle = lowerTitle.Replace(".", ""); + var lowerTitle = NormalizeSearchTitle(title); var parserTitle = lowerTitle; @@ -435,7 +443,7 @@ public List SearchForNewMovie(string title) if (parserResult != null && parserResult.PrimaryMovieTitle != title) { // Parser found something interesting! - parserTitle = parserResult.PrimaryMovieTitle.ToLower().Replace(".", " "); // TODO Update so not every period gets replaced (e.g. R.I.P.D.) + parserTitle = NormalizeSearchTitle(parserResult.PrimaryMovieTitle); if (parserResult.Year > 1800) { yearTerm = parserResult.Year.ToString(); @@ -514,7 +522,7 @@ public List SearchForNewMovie(string title) } } - var searchTerm = parserTitle.Replace("_", "+").Replace(" ", "+").Replace(".", "+"); + var searchTerm = BuildSearchTerm(parserTitle); var firstChar = searchTerm.First(); diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index 9aeb138f2ea..392be0d1ba1 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -44,30 +44,24 @@ public ReleaseInfo() [JsonIgnore] public PendingReleaseReason? PendingReleaseReason { get; set; } + // Keep private setters so serialized/manual release payloads can be deserialized safely. + private TimeSpan AgeSpan => DateTime.UtcNow - PublishDate; + public int Age { - get { return DateTime.UtcNow.Subtract(PublishDate).Days; } - - // This prevents manually downloading a release from blowing up in mono - // TODO: Is there a better way? + get { return AgeSpan.Days; } private set { } } public double AgeHours { - get { return DateTime.UtcNow.Subtract(PublishDate).TotalHours; } - - // This prevents manually downloading a release from blowing up in mono - // TODO: Is there a better way? + get { return AgeSpan.TotalHours; } private set { } } public double AgeMinutes { - get { return DateTime.UtcNow.Subtract(PublishDate).TotalMinutes; } - - // This prevents manually downloading a release from blowing up in mono - // TODO: Is there a better way? + get { return AgeSpan.TotalMinutes; } private set { } } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index ed82f001193..8f905189f81 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -43,10 +43,6 @@ public static class Parser new Regex(@"^(?(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*" + EditionRegex + @".{1,3}(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.2011.Special.Edition //TODO: Seems to slow down parsing heavily! - /*new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(19|20)\d{2}(?!p|i|(19|20)\d{2}|\]|\W(19|20)\d{2})))+(\W+|_|$)(?!\\)\(?(?<edition>(((Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Ultimate|Final(?=(.(Cut|Edition|Version)))|Extended|Rogue|Special|Despecialized|\d{2,3}(th)?.Anniversary)(.(Cut|Edition|Version))?(.(Extended|Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit))?|((Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit|Edition|Restored|((2|3|4)in1))))))\)?", - RegexOptions.IgnoreCase | RegexOptions.Compiled),*/ - // Normal movie format, e.g: Mission.Impossible.3.2011 new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|(1(8|9)|20)\d{2}|\]|\W(1(8|9)|20)\d{2})))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -280,6 +276,7 @@ public static ParsedMovieInfo ParseMovieTitle(string title, bool isDir = false) if (result.Edition.IsNullOrWhiteSpace()) { + // Handles edition-after-year cases without another movie-title regex pass. result.Edition = ParseEdition(simpleReleaseTitle); Logger.Debug("Edition parsed: {0}", result.Edition); } diff --git a/src/NzbDrone.Core/Parser/ParserCommon.cs b/src/NzbDrone.Core/Parser/ParserCommon.cs index 40dcaa8f2d0..d82197a29d4 100644 --- a/src/NzbDrone.Core/Parser/ParserCommon.cs +++ b/src/NzbDrone.Core/Parser/ParserCommon.cs @@ -9,7 +9,7 @@ internal static class ParserCommon internal static readonly RegexReplace[] PreSubstitutionRegex = System.Array.Empty<RegexReplace>(); // Valid TLDs http://data.iana.org/TLD/tlds-alpha-by-domain.txt - internal static readonly RegexReplace WebsitePrefixRegex = new (@"^(?:(?:\[|\()\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*(?:\]|\))|[ -]{2,})[ -]*", + internal static readonly RegexReplace WebsitePrefixRegex = new (@"^(?:\[url(?:=[^\]]+)?\]\s*)?(?:(?:\[|\()\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*(?:\]|\)|\[/url\])|[ -]{2,})[ -]*", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 828b6492a79..84fc98bc1e5 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -761,8 +761,6 @@ private static QualityModel ParseQualityModifiers(string name, string normalized result.RevisionDetectionSource = QualityDetectionSource.Name; } - // TODO: re-enable this when we have a reliable way to determine real - // TODO: Only treat it as a real if it comes AFTER the season/episode number var realRegexResult = RealRegex.Matches(name); if (realRegexResult.Count > 0)