├── .github ├── FUNDING.yml └── workflows │ ├── ReleaseNotes.md │ ├── build.yml │ ├── codeql.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── PlexMatchGenerator.sln ├── PlexMatchGenerator ├── Abstractions │ └── IMediaPath.cs ├── Constants │ ├── CommandConstants.cs │ ├── FileConstants.cs │ ├── HttpConstants.cs │ ├── LogSourceConstants.cs │ ├── MediaConstants.cs │ ├── MessageConstants.cs │ ├── PlexApiConstants.cs │ └── RegexConstants.cs ├── Helpers │ ├── ArgumentHelper.cs │ ├── CommandHelper.cs │ ├── PlexMatchFileHelper.cs │ └── RestClientHelper.cs ├── Models │ ├── PlexMatchFileType.cs │ ├── PlexMatchInfo.cs │ └── ProcessingResults.cs ├── Options │ ├── GeneratorOptions.cs │ └── RootPathOptions.cs ├── PlexMatchGenerator.csproj ├── Program.cs ├── RestModels │ ├── Library.cs │ ├── MediaItem.cs │ ├── MediaItemInfo.cs │ └── ShowOrdering.cs ├── Services │ └── IGeneratorService.cs └── Startup.cs └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: johnkiddjr 2 | custom: "https://paypal.me/kiddclan" -------------------------------------------------------------------------------- /.github/workflows/ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## What's Changed 4 | 5 | * Add library command line switch to restrict to only specified libraries per [#38](https://github.com/johnkiddjr/PlexMatch-File-Generator/issues/38) 6 | * Add show command line switch to restrict to only specified media items per [#37](https://github.com/johnkiddjr/PlexMatch-File-Generator/issues/37) 7 | * Add seasonprocessing command line switch to enable per season processing of TV Shows per [#39](https://github.com/johnkiddjr/PlexMatch-File-Generator/issues/39) 8 | 9 | ## Known Bugs 10 | 11 | * None! If you encounter any bugs, please open an [issue](https://github.com/johnkiddjr/PlexMatch-File-Generator/issues/new)! 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - feature/* 7 | - bugfix/* 8 | pull_request: 9 | branches: 10 | - develop 11 | - main 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v2 22 | with: 23 | dotnet-version: 6.0.x 24 | - name: Restore dependencies 25 | run: dotnet restore 26 | - name: Build 27 | run: dotnet build --no-restore 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: 17 | - feature/* 18 | pull_request: 19 | branches: 20 | - develop 21 | - main 22 | schedule: 23 | - cron: '27 2 * * 2' 24 | 25 | jobs: 26 | analyze: 27 | name: Analyze 28 | runs-on: ubuntu-latest 29 | permissions: 30 | actions: read 31 | contents: read 32 | security-events: write 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [ 'csharp' ] 38 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 39 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v2 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 https://git.io/JvXDl 62 | 63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 64 | # and modify them (or add more) to build your code if your project 65 | # uses a compiled language 66 | 67 | #- run: | 68 | # make bootstrap 69 | # make release 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Setup .NET 15 | uses: actions/setup-dotnet@v2 16 | with: 17 | dotnet-version: 6.0.x 18 | - name: get tag 19 | run: echo "VTAG=$(echo ${GITHUB_REF:10})" >> $GITHUB_ENV 20 | - name: get version tag 21 | run: echo "VERSION=$(echo ${GITHUB_REF:11})" >> $GITHUB_ENV 22 | - name: Restore Dependencies 23 | run: dotnet restore 24 | - name: build 25 | run: dotnet build -c Release --no-restore 26 | - name: publish Windows version 27 | run: dotnet publish -c Release -o ./publish -r win-x86 -p:Version="${{env.VERSION}}" -p:PublishSingleFile=true --self-contained 28 | - name: rename Windows executable 29 | run: mv ./publish/PlexMatchGenerator.exe ./publish/PlexMatchGenerator-Windows-x86.exe 30 | - name: publish Linux version 31 | run: dotnet publish -c Release -o ./publish -r linux-x64 -p:Version="${{env.VERSION}}" -p:PublishSingleFile=true --self-contained 32 | - name: rename Linux executable 33 | run: mv ./publish/PlexMatchGenerator ./publish/PlexMatchGenerator-Linux-x64 34 | - name: publish Mac version 35 | run: dotnet publish -c Release -o ./publish -r osx-x64 -p:Version="${{env.VERSION}}" -p:PublishSingleFile=true --self-contained 36 | - name: rename Mac OSX executable 37 | run: mv ./publish/PlexMatchGenerator ./publish/PlexMatchGenerator-MacOSX-x64 38 | - name: remove debug artifacts 39 | run: rm ./publish/*.pdb -fr 40 | - name: upload artifacts 41 | uses: actions/upload-artifact@v3 42 | with: 43 | name: publish_artifacts 44 | path: ./publish 45 | - name: create release 46 | id: create_release 47 | uses: actions/create-release@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | tag_name: ${{ github.ref }} 52 | release_name: ${{ env.VTAG }} 53 | body_path: ./.github/workflows/ReleaseNotes.md 54 | draft: false 55 | prerelease: false 56 | - name: upload linux artifact 57 | uses: actions/upload-release-asset@v1 58 | env: 59 | GITHUB_TOKEN: ${{ github.token }} 60 | with: 61 | upload_url: ${{ steps.create_release.outputs.upload_url }} 62 | asset_path: ./publish/PlexMatchGenerator-Linux-x64 63 | asset_name: PlexMatchGenerator-Linux-x64 64 | asset_content_type: application/octet-stream 65 | - name: upload windows artifact 66 | uses: actions/upload-release-asset@v1 67 | env: 68 | GITHUB_TOKEN: ${{ github.token }} 69 | with: 70 | upload_url: ${{ steps.create_release.outputs.upload_url }} 71 | asset_path: ./publish/PlexMatchGenerator-Windows-x86.exe 72 | asset_name: PlexMatchGenerator-Windows-x86.exe 73 | asset_content_type: application/octet-stream 74 | - name: upload osx artifact 75 | uses: actions/upload-release-asset@v1 76 | env: 77 | GITHUB_TOKEN: ${{ github.token }} 78 | with: 79 | upload_url: ${{ steps.create_release.outputs.upload_url }} 80 | asset_path: ./publish/PlexMatchGenerator-MacOSX-x64 81 | asset_name: PlexMatchGenerator-MacOSX-x64 82 | asset_content_type: application/octet-stream -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | #Properties 35 | [Pp]roperties/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 John Kidd Jr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PlexMatchGenerator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32228.430 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexMatchGenerator", "PlexMatchGenerator\PlexMatchGenerator.csproj", "{F10E21DB-7BAB-454F-98FB-20CF98EC9A1F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {F10E21DB-7BAB-454F-98FB-20CF98EC9A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {F10E21DB-7BAB-454F-98FB-20CF98EC9A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {F10E21DB-7BAB-454F-98FB-20CF98EC9A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {F10E21DB-7BAB-454F-98FB-20CF98EC9A1F}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {09283647-7A74-4FE2-B264-0352EDA8DCC8} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Abstractions/IMediaPath.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Abstractions 2 | { 3 | public interface IMediaPath 4 | { 5 | public string MediaItemPath { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Constants/CommandConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Constants 2 | { 3 | public class CommandConstants 4 | { 5 | public const string RootDescription = "PlexMatch File Generator - Generates .plexmatch files for existing library"; 6 | 7 | public const string TokenCommandDescription = "Plex Server Token more information on getting this at: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/"; 8 | public const string TokenCommandHelpName = "Plex Server Token"; 9 | public const string TokenCommandName = "token"; 10 | public const string TokenCommandShortName = "t"; 11 | public const string TokenCommandUnixLong = $"--{TokenCommandName}"; 12 | public const string TokenCommandUnixShort = $"-{TokenCommandShortName}"; 13 | public const string TokenCommandWindowsLong = $"/{TokenCommandName}"; 14 | public const string TokenCommandWindowsShort = $"/{TokenCommandShortName}"; 15 | 16 | public const string LibraryCommandDescription = "Name of the library within Plex to generate .plexmatch files for, this command can be passed multiple times."; 17 | public const string LibraryCommandHelpName = "Plex Library Name"; 18 | public const string LibraryCommandName = "library"; 19 | public const string LibraryCommandShortName = "lib"; 20 | public const string LibraryCommandUnixLong = $"--{LibraryCommandName}"; 21 | public const string LibraryCommandUnixShort = $"-{LibraryCommandShortName}"; 22 | public const string LibraryCommandWindowsLong = $"/{LibraryCommandName}"; 23 | public const string LibraryCommandWindowsShort = $"/{LibraryCommandShortName}"; 24 | 25 | public const string ShowCommandDescription = "Name of the show within Plex to generate .plexmatch files for, this command can be passed multiple times."; 26 | public const string ShowCommandHelpName = "Plex Show Name"; 27 | public const string ShowCommandName = "show"; 28 | public const string ShowCommandShortName = "s"; 29 | public const string ShowCommandUnixLong = $"--{ShowCommandName}"; 30 | public const string ShowCommandUnixShort = $"-{ShowCommandShortName}"; 31 | public const string ShowCommandWindowsLong = $"/{ShowCommandName}"; 32 | public const string ShowCommandWindowsShort = $"/{ShowCommandShortName}"; 33 | 34 | public const string UrlCommandDescription = "URL to access your Plex server. Must include the http or https portion"; 35 | public const string UrlCommandHelpName = "Plex Server URL"; 36 | public const string UrlCommandName = "url"; 37 | public const string UrlCommandShortName = "u"; 38 | public const string UrlCommandUnixLong = $"--{UrlCommandName}"; 39 | public const string UrlCommandUnixShort = $"-{UrlCommandShortName}"; 40 | public const string UrlCommandWindowsLong = $"/{UrlCommandName}"; 41 | public const string UrlCommandWindowsShort = $"/{UrlCommandShortName}"; 42 | 43 | public const string RootPathCommandDescription = "Sets the root path used to be different than what your Plex server returns, this option can be set more than once"; 44 | public const string RootPathCommandHelpName = "Root Path Map"; 45 | public const string RootPathCommandName = "root"; 46 | public const string RootPathCommandShortName = "r"; 47 | public const string RootPathCommandUnixLong = $"--{RootPathCommandName}"; 48 | public const string RootPathCommandUnixShort = $"-{RootPathCommandShortName}"; 49 | public const string RootPathCommandWindowsLong = $"/{RootPathCommandName}"; 50 | public const string RootPathCommandWindowsShort = $"/{RootPathCommandShortName}"; 51 | 52 | public const string LogPathCommandDescription = "Outputs the log to file at the path specified, log file will be named plexmatch.log in the directory specified"; 53 | public const string LogPathCommandHelpName = "Log Path"; 54 | public const string LogPathCommandName = "log"; 55 | public const string LogPathCommandShortName = "l"; 56 | public const string LogPathCommandUnixLong = $"--{LogPathCommandName}"; 57 | public const string LogPathCommandUnixShort = $"-{LogPathCommandShortName}"; 58 | public const string LogPathCommandWindowsLong = $"/{LogPathCommandName}"; 59 | public const string LogPathCommandWindowsShort = $"/{LogPathCommandShortName}"; 60 | 61 | public const string NoOverwriteCommandDescription = "Disables automatic overwriting"; 62 | public const string NoOverwriteCommandHelpName = "Disable Overwrite"; 63 | public const string NoOverwriteCommandName = "nooverwrite"; 64 | public const string NoOverwriteCommandShortName = "no"; 65 | public const string NoOverwriteCommandUnixLong = $"--{NoOverwriteCommandName}"; 66 | public const string NoOverwriteCommandUnixShort = $"-{NoOverwriteCommandShortName}"; 67 | public const string NoOverwriteCommandWindowsLong = $"/{NoOverwriteCommandName}"; 68 | public const string NoOverwriteCommandWindowsShort = $"/{NoOverwriteCommandShortName}"; 69 | 70 | public const string PageSizeCommandDescription = "Sets page size to request (default: 20)"; 71 | public const string PageSizeCommandHelpName = "Sets the page size"; 72 | public const string PageSizeCommandName = "pagesize"; 73 | public const string PageSizeCommandShortName = "ps"; 74 | public const string PageSizeCommandUnixLong = $"--{PageSizeCommandName}"; 75 | public const string PageSizeCommandUnixShort = $"-{PageSizeCommandShortName}"; 76 | public const string PageSizeCommandWindowsLong = $"/{PageSizeCommandName}"; 77 | public const string PageSizeCommandWindowsShort = $"/{PageSizeCommandShortName}"; 78 | 79 | public const string PerSeasonProcessingCommandDescription = "Enables per season process for all items, default behavior processes seasons only when non-default episode ordering is used."; 80 | public const string PerSeasonProcessingCommandHelpName = "Enables per season processing"; 81 | public const string PerSeasonProcessingCommandName = "seasonprocessing"; 82 | public const string PerSeasonProcessingCommandShortName = "sp"; 83 | public const string PerSeasonProcessingCommandUnixLong = $"--{PerSeasonProcessingCommandName}"; 84 | public const string PerSeasonProcessingCommandUnixShort = $"-{PerSeasonProcessingCommandShortName}"; 85 | public const string PerSeasonProcessingCommandWindowsLong = $"/{PerSeasonProcessingCommandName}"; 86 | public const string PerSeasonProcessingCommandWindowsShort = $"/{PerSeasonProcessingCommandShortName}"; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Constants/FileConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Constants 2 | { 3 | public class FileConstants 4 | { 5 | public const string LogFileName = "plexmatch.log"; 6 | public const string PlexMatchFileName = ".plexmatch"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Constants/HttpConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Constants 2 | { 3 | public class HttpConstants 4 | { 5 | public const string PlexTokenHeaderName = "X-Plex-Token"; 6 | public const string ApplicationJson = "application/json"; 7 | public const string UnsecureProtocol = "http://"; 8 | public const string SecureProtocol = "https://"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Constants/LogSourceConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Constants 2 | { 3 | public class LogSourceConstants 4 | { 5 | public const string Microsoft = "Microsoft"; 6 | public const string System = "System"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Constants/MediaConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Constants 2 | { 3 | public class MediaConstants 4 | { 5 | public const string PlexMatchTitleHeader = "Title: "; 6 | public const string PlexMatchYearHeader = "Year: "; 7 | public const string PlexMatchGuidHeader = "Guid: "; 8 | public const string PlexMatchSeasonHeader = "Season: "; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Constants/MessageConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Constants 2 | { 3 | public class MessageConstants 4 | { 5 | public const string EnterPlexToken = "Please enter your Plex Token: "; 6 | public const string TokenInvalid = "Plex Server Token entered must be valid and set"; 7 | public const string EnterPlexServerUrl = "Please enter your Plex Server URL: "; 8 | public const string UrlInvalid = "Plex Server URL must be set, and must start with either http:// or https://"; 9 | public const string ArgumentsMissing = "Plex Server URL and Plex Token are required! Exiting..."; 10 | public const string FolderMissingOrInvalid = "Missing or Invalid Folder: {path}"; 11 | public const string CompletedMessage = "Operation Completed"; 12 | public const string PlexMatchWritten = ".plexmatch file written successfully for: {mediaTitle}"; 13 | public const string PlexMatchSeasonWritten = ".plexmatch file written successfully for: {mediaTitle} Season {seasonNumber} in path {seasonPath}"; 14 | public const string NoMediaFound = "No media location found for: {mediaItemTitle}"; 15 | public const string NoLocationInfoForItemFound = "Item with title {itemTitle} and ID {itemId} returned no location information"; 16 | public const string LoggerAttachedMessage = "Logger attached. Startup complete. Running file generator..."; 17 | public const string LibrariesNoResults = "No data or malformed data received from server when querying for libraries"; 18 | public const string LibraryItemsNoResults = "Library {libraryName} of type {libraryType} with ID {libraryID} returned no items"; 19 | public const string LibraryProcessedSuccess = "Processed results for {library}: {records} processed"; 20 | public const string LibraryProcessedSuccessWithSkipped = "Processed results for {library}: {records} processed, {skipped} skipped"; 21 | public const string NoWriteBecauseDisabled = ".plexmatch file not written because overwrite disabled for item: {mediaTitle}"; 22 | public const string LibrarySkipped = "Library {library} skipped because it is not in the list of libraries to process"; 23 | public const string ShowSkipped = "Show {show} skipped because it is not in the list of shows to process"; 24 | 25 | //exception messages 26 | public const string ExceptionHeaderMessage = "An unhandeled exception occurred details below:"; 27 | public const string ExceptionTypeMessage = "Exception Type: {exceptionType}"; 28 | public const string ExceptionMessageMessage = "Exception Message: {exceptionMessage}"; 29 | public const string ExceptionInnerExceptionTypeMessage = "Inner Exception Type: {innerType}"; 30 | public const string ExceptionInnerExceptionMessageMessage = "Inner Exception Message: {innerMessage}"; 31 | public const string ExceptionSourceMessage = "Exception Source: {exceptionSource}"; 32 | public const string ExceptionStackTraceMessage = "Exception Stack Trace: {stackTrace}"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Constants/PlexApiConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Constants 2 | { 3 | public class PlexApiConstants 4 | { 5 | public const string MetaDataRequestUrl = "library/metadata"; 6 | public const string MediaItemChildren = "children"; 7 | public const string LibrarySectionsRequestUrl = "library/sections"; 8 | public const string SearchAll = "all"; 9 | public const string MovieLibraryType = "movie"; 10 | public const string TVLibraryType = "show"; 11 | public const string MusicLibraryType = "artist"; 12 | public const string ContainerSize = "X-Plex-Container-Size"; 13 | public const string ContainerStart = "X-Plex-Container-Start"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Constants/RegexConstants.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Constants 2 | { 3 | public class RegexConstants 4 | { 5 | public const string RootPathMatchPattern = "([a-zA-Z\\\\\\/]:[^:?*\"><|\\r\\n\\t\\f\\v]*|\\\\{2}[a-zA-Z0-9]*:?[0-9]{0,5}[^:?*\"><|\\r\\n\\t\\f\\v]*|[^:?*\"><|\\r\\n\\t\\f\\v]+)"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Helpers/ArgumentHelper.cs: -------------------------------------------------------------------------------- 1 | using PlexMatchGenerator.Constants; 2 | using PlexMatchGenerator.Options; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace PlexMatchGenerator.Helpers 6 | { 7 | public class ArgumentHelper 8 | { 9 | public static GeneratorOptions ProcessCommandLineResults( 10 | string plexToken, 11 | string plexUrl, 12 | List rootPaths, 13 | string logPath, 14 | bool noOverwrite, 15 | int pageSize, 16 | List libraries, 17 | List shows, 18 | bool seasonProcessing) 19 | { 20 | //ensure we end the path with a slash 21 | if (logPath != null && !logPath.EndsWith("\\") && !logPath.EndsWith('/')) 22 | { 23 | //make sure we use the correct slash if we need to use it 24 | bool useBackslash = logPath.Count(x => x.Equals('\\')) > logPath.Count(x => x.Equals('/')); 25 | 26 | logPath += useBackslash ? "\\" : "/"; 27 | } 28 | 29 | return new GeneratorOptions 30 | { 31 | LogPath = logPath, 32 | PlexServerUrl = plexUrl, 33 | PlexServerToken = plexToken, 34 | RootPaths = GenerateRootPaths(rootPaths), 35 | NoOverwrite = noOverwrite, 36 | ItemsPerPage = pageSize == 0 ? 20 : pageSize, 37 | LibraryNames = libraries, 38 | ShowNames = shows, 39 | EnablePerSeasonProcessing = seasonProcessing 40 | }; 41 | } 42 | 43 | private static List GenerateRootPaths(List rootMaps) 44 | { 45 | if (!rootMaps.Any()) 46 | { 47 | return new List(); 48 | } 49 | 50 | var rootPathMaps = new List(); 51 | 52 | foreach (var rootMap in rootMaps) 53 | { 54 | var pathMatches = Regex.Matches(rootMap, RegexConstants.RootPathMatchPattern); 55 | 56 | if (pathMatches.Count > 1) 57 | { 58 | var newRootPath = new RootPathOptions(); 59 | 60 | foreach (Match match in pathMatches) 61 | { 62 | if (string.IsNullOrWhiteSpace(match.Value)) 63 | { 64 | continue; 65 | } 66 | 67 | if (string.IsNullOrWhiteSpace(newRootPath.HostRootPath)) 68 | { 69 | newRootPath.HostRootPath = match.Value; 70 | } 71 | else 72 | { 73 | newRootPath.PlexRootPath = match.Value; 74 | break; 75 | } 76 | } 77 | 78 | rootPathMaps.Add(newRootPath); 79 | } 80 | } 81 | 82 | return rootPathMaps; 83 | } 84 | 85 | public static bool ValidatePlexUrl(string plexUrl) 86 | { 87 | if (!plexUrl.EndsWith("/")) 88 | { 89 | plexUrl += "/"; 90 | } 91 | 92 | return plexUrl.StartsWith(HttpConstants.UnsecureProtocol) || plexUrl.StartsWith(HttpConstants.SecureProtocol); 93 | } 94 | 95 | // This stub exists for potential future expansion only 96 | public static bool ValidatePlexToken(string plexToken) => 97 | !string.IsNullOrEmpty(plexToken); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Helpers/CommandHelper.cs: -------------------------------------------------------------------------------- 1 | using PlexMatchGenerator.Constants; 2 | using PlexMatchGenerator.Options; 3 | using System.CommandLine; 4 | 5 | namespace PlexMatchGenerator.Helpers 6 | { 7 | public class CommandHelper 8 | { 9 | public static async Task GenerateRootCommandAndExecuteHandler(string[] args, Func> handler) 10 | { 11 | var rootCommand = new RootCommand(CommandConstants.RootDescription); 12 | 13 | var tokenAliases = new string[] 14 | { 15 | CommandConstants.TokenCommandUnixLong, 16 | CommandConstants.TokenCommandUnixShort, 17 | CommandConstants.TokenCommandWindowsLong, 18 | CommandConstants.TokenCommandWindowsShort, 19 | }; 20 | 21 | var tokenOption = GenerateOption( 22 | tokenAliases, 23 | CommandConstants.TokenCommandDescription, 24 | CommandConstants.TokenCommandHelpName, 25 | CommandConstants.TokenCommandName, 26 | true); 27 | 28 | var urlAliases = new string[] 29 | { 30 | CommandConstants.UrlCommandUnixLong, 31 | CommandConstants.UrlCommandUnixShort, 32 | CommandConstants.UrlCommandWindowsLong, 33 | CommandConstants.UrlCommandWindowsShort 34 | }; 35 | 36 | var urlOption = GenerateOption( 37 | urlAliases, 38 | CommandConstants.UrlCommandDescription, 39 | CommandConstants.UrlCommandHelpName, 40 | CommandConstants.UrlCommandName, 41 | true); 42 | 43 | var rootAliases = new string[] 44 | { 45 | CommandConstants.RootPathCommandUnixLong, 46 | CommandConstants.RootPathCommandUnixShort, 47 | CommandConstants.RootPathCommandWindowsLong, 48 | CommandConstants.RootPathCommandWindowsShort 49 | }; 50 | 51 | var rootOption = GenerateOption>( 52 | rootAliases, 53 | CommandConstants.RootPathCommandDescription, 54 | CommandConstants.RootPathCommandHelpName, 55 | CommandConstants.RootPathCommandName); 56 | 57 | var logAliases = new string[] 58 | { 59 | CommandConstants.LogPathCommandUnixLong, 60 | CommandConstants.LogPathCommandUnixShort, 61 | CommandConstants.LogPathCommandWindowsLong, 62 | CommandConstants.LogPathCommandWindowsShort 63 | }; 64 | 65 | var logOption = GenerateOption( 66 | logAliases, 67 | CommandConstants.LogPathCommandDescription, 68 | CommandConstants.LogPathCommandHelpName, 69 | CommandConstants.LogPathCommandName); 70 | 71 | var noOverwriteAliases = new string[] 72 | { 73 | CommandConstants.NoOverwriteCommandUnixLong, 74 | CommandConstants.NoOverwriteCommandUnixShort, 75 | CommandConstants.NoOverwriteCommandWindowsLong, 76 | CommandConstants.NoOverwriteCommandWindowsShort 77 | }; 78 | 79 | var noOverwriteOption = GenerateOption( 80 | noOverwriteAliases, 81 | CommandConstants.NoOverwriteCommandDescription, 82 | CommandConstants.NoOverwriteCommandHelpName, 83 | CommandConstants.NoOverwriteCommandName); 84 | 85 | var pageSizeAliases = new string[] 86 | { 87 | CommandConstants.PageSizeCommandUnixLong, 88 | CommandConstants.PageSizeCommandUnixShort, 89 | CommandConstants.PageSizeCommandWindowsLong, 90 | CommandConstants.PageSizeCommandWindowsShort 91 | }; 92 | 93 | var pageSizeOption = GenerateOption( 94 | pageSizeAliases, 95 | CommandConstants.PageSizeCommandDescription, 96 | CommandConstants.PageSizeCommandHelpName, 97 | CommandConstants.PageSizeCommandName); 98 | 99 | var libraryAliases = new string[] 100 | { 101 | CommandConstants.LibraryCommandUnixLong, 102 | CommandConstants.LibraryCommandUnixShort, 103 | CommandConstants.LibraryCommandWindowsLong, 104 | CommandConstants.LibraryCommandWindowsShort 105 | }; 106 | 107 | var libraryOption = GenerateOption>( 108 | libraryAliases, 109 | CommandConstants.LibraryCommandDescription, 110 | CommandConstants.LibraryCommandHelpName, 111 | CommandConstants.LibraryCommandName); 112 | 113 | var showAliases = new string[] 114 | { 115 | CommandConstants.ShowCommandUnixLong, 116 | CommandConstants.ShowCommandUnixShort, 117 | CommandConstants.ShowCommandWindowsLong, 118 | CommandConstants.ShowCommandWindowsShort 119 | }; 120 | 121 | var showOption = GenerateOption>( 122 | showAliases, 123 | CommandConstants.ShowCommandDescription, 124 | CommandConstants.ShowCommandHelpName, 125 | CommandConstants.ShowCommandName); 126 | 127 | var perSeasonProcessingAliases = new string[] 128 | { 129 | CommandConstants.PerSeasonProcessingCommandUnixLong, 130 | CommandConstants.PerSeasonProcessingCommandUnixShort, 131 | CommandConstants.PerSeasonProcessingCommandWindowsLong, 132 | CommandConstants.PerSeasonProcessingCommandWindowsShort 133 | }; 134 | 135 | var perSeasonProcessingOption = GenerateOption( 136 | perSeasonProcessingAliases, 137 | CommandConstants.PerSeasonProcessingCommandDescription, 138 | CommandConstants.PerSeasonProcessingCommandHelpName, 139 | CommandConstants.PerSeasonProcessingCommandName); 140 | 141 | rootCommand.AddOption(tokenOption); 142 | rootCommand.AddOption(urlOption); 143 | rootCommand.AddOption(rootOption); 144 | rootCommand.AddOption(logOption); 145 | rootCommand.AddOption(pageSizeOption); 146 | rootCommand.AddOption(noOverwriteOption); 147 | rootCommand.AddOption(libraryOption); 148 | rootCommand.AddOption(showOption); 149 | rootCommand.AddOption(perSeasonProcessingOption); 150 | 151 | rootCommand.SetHandler( 152 | async ( 153 | string token, 154 | string url, 155 | List rootPaths, 156 | string log, 157 | int pageSize, 158 | bool overwrite, 159 | List libraries, 160 | List shows, 161 | bool seasonProcessing) => 162 | { 163 | var generatorOptions = ArgumentHelper.ProcessCommandLineResults( 164 | token, 165 | url, 166 | rootPaths, 167 | log, 168 | overwrite, 169 | pageSize, 170 | libraries, 171 | shows, 172 | seasonProcessing); 173 | await handler(generatorOptions, args); 174 | }, 175 | tokenOption, 176 | urlOption, 177 | rootOption, 178 | logOption, 179 | pageSizeOption, 180 | noOverwriteOption, 181 | libraryOption, 182 | showOption, 183 | perSeasonProcessingOption); 184 | 185 | return await rootCommand.InvokeAsync(args); 186 | } 187 | 188 | private static Option GenerateOption(string[] aliases, string description, string helpName, string name, bool required = false) 189 | { 190 | var option = new Option(aliases, description); 191 | option.ArgumentHelpName = helpName; 192 | option.Name = name; 193 | option.IsRequired = required; 194 | 195 | return option; 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Helpers/PlexMatchFileHelper.cs: -------------------------------------------------------------------------------- 1 | using PlexMatchGenerator.Constants; 2 | using PlexMatchGenerator.Models; 3 | 4 | namespace PlexMatchGenerator.Helpers 5 | { 6 | public static class PlexMatchFileHelper 7 | { 8 | public static void WritePlexMatchFile(StreamWriter writer, PlexMatchInfo info) 9 | { 10 | switch (info.FileType) 11 | { 12 | case PlexMatchFileType.Season: 13 | writer.WriteLine($"{MediaConstants.PlexMatchTitleHeader}{info.MediaItemTitle}"); 14 | writer.WriteLine($"{MediaConstants.PlexMatchYearHeader}{info.MediaItemReleaseYear}"); 15 | writer.WriteLine($"{MediaConstants.PlexMatchSeasonHeader}{info.SeasonNumber}"); 16 | writer.WriteLine($"{MediaConstants.PlexMatchGuidHeader}{info.MediaItemPlexMatchGuid}"); 17 | break; 18 | case PlexMatchFileType.Main: 19 | default: 20 | writer.WriteLine($"{MediaConstants.PlexMatchTitleHeader}{info.MediaItemTitle}"); 21 | writer.WriteLine($"{MediaConstants.PlexMatchYearHeader}{info.MediaItemReleaseYear}"); 22 | writer.WriteLine($"{MediaConstants.PlexMatchGuidHeader}{info.MediaItemPlexMatchGuid}"); 23 | break; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Helpers/RestClientHelper.cs: -------------------------------------------------------------------------------- 1 | using PlexMatchGenerator.Constants; 2 | using RestSharp; 3 | using RestSharp.Serializers.NewtonsoftJson; 4 | 5 | namespace PlexMatchGenerator.Helpers 6 | { 7 | public class RestClientHelper 8 | { 9 | public static RestClient GenerateClient(string plexUrl, string plexToken) 10 | { 11 | var client = new RestClient(plexUrl); 12 | client.UseNewtonsoftJson(); 13 | client.AddDefaultHeader(KnownHeaders.Accept, HttpConstants.ApplicationJson); 14 | client.AddDefaultHeader(HttpConstants.PlexTokenHeaderName, plexToken); 15 | 16 | return client; 17 | } 18 | 19 | public static async Task CreateAndGetRestResponse(RestClient client, string resource, Method method, Dictionary additionalHeaders = null) 20 | { 21 | var request = new RestRequest(resource, method); 22 | 23 | if (additionalHeaders != null) 24 | { 25 | foreach (var header in additionalHeaders) 26 | { 27 | request.AddHeader(header.Key, header.Value); 28 | } 29 | } 30 | 31 | var response = await client.ExecuteGetAsync(request); 32 | 33 | return response.Data; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Models/PlexMatchFileType.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Models 2 | { 3 | public enum PlexMatchFileType 4 | { 5 | Main, 6 | Season 7 | } 8 | } -------------------------------------------------------------------------------- /PlexMatchGenerator/Models/PlexMatchInfo.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Models 2 | { 3 | public class PlexMatchInfo 4 | { 5 | public PlexMatchFileType FileType { get; set; } 6 | public string MediaItemTitle { get; set; } 7 | public int MediaItemReleaseYear { get; set; } 8 | public string MediaItemPlexMatchGuid { get; set; } 9 | public int SeasonNumber { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Models/ProcessingResults.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Models 2 | { 3 | public record ProcessingResults 4 | { 5 | public bool Success { get; set; } 6 | public int RecordsProcessed { get; set; } 7 | public int RecordsSkipped { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Options/GeneratorOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Options 2 | { 3 | public class GeneratorOptions 4 | { 5 | public string PlexServerUrl { get; set; } 6 | public string PlexServerToken { get; set; } 7 | public IEnumerable RootPaths { get; set; } 8 | public string LogPath { get; set; } 9 | public bool NoOverwrite { get; set; } 10 | public int ItemsPerPage { get; set; } 11 | public List LibraryNames { get; set; } 12 | public List ShowNames { get; set; } 13 | public bool EnablePerSeasonProcessing { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Options/RootPathOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.Options 2 | { 3 | public class RootPathOptions 4 | { 5 | public string HostRootPath { get; set; } 6 | public string PlexRootPath { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PlexMatchGenerator/PlexMatchGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | disable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using PlexMatchGenerator.Constants; 4 | using PlexMatchGenerator.Helpers; 5 | using PlexMatchGenerator.Options; 6 | using PlexMatchGenerator.Services; 7 | using Serilog; 8 | using Serilog.Enrichers; 9 | 10 | namespace PlexMatchGenerator 11 | { 12 | class Program 13 | { 14 | static async Task Main(string[] args) 15 | { 16 | return await CommandHelper.GenerateRootCommandAndExecuteHandler(args, Run); 17 | } 18 | 19 | static async Task Run(GeneratorOptions generatorOptions, string[] args) 20 | { 21 | var startup = new Startup(); 22 | 23 | if (string.IsNullOrEmpty(generatorOptions.LogPath)) 24 | { 25 | Log.Logger = new LoggerConfiguration() 26 | .MinimumLevel.Override(LogSourceConstants.Microsoft, Serilog.Events.LogEventLevel.Information) 27 | .MinimumLevel.Override(LogSourceConstants.System, Serilog.Events.LogEventLevel.Warning) 28 | .MinimumLevel.Information() 29 | .Enrich.With(new MachineNameEnricher()) 30 | .WriteTo.Console() 31 | .CreateLogger(); 32 | } 33 | else 34 | { 35 | Log.Logger = new LoggerConfiguration() 36 | .MinimumLevel.Override(LogSourceConstants.Microsoft, Serilog.Events.LogEventLevel.Information) 37 | .MinimumLevel.Override(LogSourceConstants.System, Serilog.Events.LogEventLevel.Warning) 38 | .MinimumLevel.Information() 39 | .Enrich.With(new MachineNameEnricher()) 40 | .WriteTo.Console() 41 | .WriteTo.File(path: $"{generatorOptions.LogPath}{FileConstants.LogFileName}") 42 | .CreateLogger(); 43 | } 44 | 45 | Log.Logger.Information(MessageConstants.LoggerAttachedMessage); 46 | 47 | var host = Host.CreateDefaultBuilder(args) 48 | .ConfigureServices((context, services) => 49 | { 50 | startup.ConfigureServices(services); 51 | }) 52 | .UseSerilog() 53 | .Build(); 54 | 55 | var svc = ActivatorUtilities.GetServiceOrCreateInstance(host.Services); 56 | 57 | return await svc.Run(generatorOptions); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /PlexMatchGenerator/RestModels/Library.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PlexMatchGenerator.RestModels 4 | { 5 | public class LibraryRoot 6 | { 7 | [JsonProperty("MediaContainer")] 8 | public LibraryContainer LibraryContainer { get; set; } 9 | } 10 | 11 | public class LibraryContainer 12 | { 13 | [JsonProperty("Directory")] 14 | public List Libraries { get; set; } 15 | } 16 | 17 | public class Library 18 | { 19 | [JsonProperty("key")] 20 | public string LibraryId { get; set; } 21 | [JsonProperty("type")] 22 | public string LibraryType { get; set; } 23 | [JsonProperty("title")] 24 | public string LibraryName { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /PlexMatchGenerator/RestModels/MediaItem.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PlexMatchGenerator.RestModels 4 | { 5 | public class MediaItemRoot 6 | { 7 | [JsonProperty("MediaContainer")] 8 | public MediaItemContainer MediaItemContainer { get; set; } 9 | } 10 | 11 | public class MediaItemContainer 12 | { 13 | [JsonProperty("Metadata")] 14 | public List MediaItems { get; set; } 15 | } 16 | 17 | public class MediaItem 18 | { 19 | [JsonProperty("index")] 20 | public int SeasonNumber { get; set; } 21 | [JsonProperty("type")] 22 | public string MediaType { get; set; } 23 | [JsonProperty("ratingKey")] 24 | public string MediaItemId { get; set; } 25 | [JsonProperty("guid")] 26 | public string MediaItemPlexMatchGuid { get; set; } 27 | [JsonProperty("title")] 28 | public string MediaItemTitle { get; set; } 29 | [JsonProperty("year")] 30 | public int MediaItemReleaseYear { get; set; } 31 | } 32 | } -------------------------------------------------------------------------------- /PlexMatchGenerator/RestModels/MediaItemInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using PlexMatchGenerator.Abstractions; 3 | 4 | namespace PlexMatchGenerator.RestModels 5 | { 6 | 7 | public class MediaItemInfoRoot 8 | { 9 | [JsonProperty("MediaContainer")] 10 | public MediaItemInfoContainer MediaItemInfoContainer { get; set; } 11 | } 12 | 13 | public class MediaItemInfoContainer 14 | { 15 | [JsonProperty("Metadata")] 16 | public List MediaItemInfos { get; set; } 17 | } 18 | 19 | public class MediaItemInfo 20 | { 21 | [JsonProperty("Location")] 22 | public List MediaItemLocations { get; set; } 23 | [JsonProperty("Media")] 24 | public List MediaInfos { get; set; } 25 | [JsonProperty("type")] 26 | public string MediaType { get; set; } 27 | [JsonProperty("index")] // from my testing this index is always the season number... but I fear it may not always be true 28 | public int SeasonNumber { get; set; } 29 | [JsonIgnore] 30 | public ShowOrdering ShowOrdering 31 | { 32 | get 33 | { 34 | if (string.IsNullOrEmpty(_ordering)) 35 | { 36 | return ShowOrdering.Default; 37 | } 38 | return _ordering switch 39 | { 40 | "absolute" => ShowOrdering.TVDBAbsolute, 41 | "aired" => ShowOrdering.TVDBAired, 42 | "dvd" => ShowOrdering.TVDBDVD, 43 | "tmdb" => ShowOrdering.TMDBAired, 44 | _ => ShowOrdering.Default 45 | }; 46 | } 47 | } 48 | 49 | [JsonProperty("showOrdering")] 50 | private string _ordering; 51 | } 52 | 53 | public class MediaItemLocation: IMediaPath 54 | { 55 | [JsonProperty("path")] 56 | public string MediaItemPath { get; set; } 57 | } 58 | 59 | public class MediaInfo 60 | { 61 | [JsonProperty("Part")] 62 | public List MediaParts { get; set; } 63 | } 64 | 65 | public class MediaInfoPart: IMediaPath 66 | { 67 | [JsonProperty("file")] 68 | public string MediaItemPath { get; set; } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /PlexMatchGenerator/RestModels/ShowOrdering.cs: -------------------------------------------------------------------------------- 1 | namespace PlexMatchGenerator.RestModels 2 | { 3 | public enum ShowOrdering 4 | { 5 | Default, 6 | TMDBAired, 7 | TVDBAired, 8 | TVDBDVD, 9 | TVDBAbsolute 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Services/IGeneratorService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Options; 3 | using PlexMatchGenerator.Abstractions; 4 | using PlexMatchGenerator.Constants; 5 | using PlexMatchGenerator.Helpers; 6 | using PlexMatchGenerator.Models; 7 | using PlexMatchGenerator.Options; 8 | using PlexMatchGenerator.RestModels; 9 | using RestSharp; 10 | 11 | namespace PlexMatchGenerator.Services 12 | { 13 | public interface IGeneratorService 14 | { 15 | public Task Run(GeneratorOptions options); 16 | } 17 | 18 | public class GeneratorService : IGeneratorService 19 | { 20 | private readonly ILogger logger; 21 | 22 | public GeneratorService(ILogger logger) 23 | { 24 | this.logger = logger; 25 | } 26 | 27 | public async Task Run(GeneratorOptions options) 28 | { 29 | if (string.IsNullOrEmpty(options.PlexServerToken)) 30 | { 31 | Console.WriteLine(MessageConstants.EnterPlexToken); 32 | options.PlexServerToken = Console.ReadLine(); 33 | } 34 | 35 | if (!ArgumentHelper.ValidatePlexToken(options.PlexServerToken)) 36 | { 37 | logger.LogCritical(MessageConstants.TokenInvalid); 38 | return 1; 39 | } 40 | 41 | if (string.IsNullOrEmpty(options.PlexServerUrl)) 42 | { 43 | Console.WriteLine(MessageConstants.EnterPlexServerUrl); 44 | options.PlexServerUrl = Console.ReadLine(); 45 | } 46 | 47 | if (!ArgumentHelper.ValidatePlexUrl(options.PlexServerUrl)) 48 | { 49 | logger.LogCritical(MessageConstants.UrlInvalid); 50 | return 1; 51 | } 52 | 53 | // connect to plex and get a list of libraries 54 | try 55 | { 56 | var client = RestClientHelper.GenerateClient(options.PlexServerUrl, options.PlexServerToken); 57 | 58 | var libraryRoot = await RestClientHelper.CreateAndGetRestResponse(client, PlexApiConstants.LibrarySectionsRequestUrl, Method.Get); 59 | 60 | var libraries = libraryRoot?.LibraryContainer?.Libraries; 61 | 62 | if (libraries is null) 63 | { 64 | logger.LogError(MessageConstants.LibrariesNoResults); 65 | return 1; 66 | } 67 | 68 | // step through the libraries and get a list of the items and their folders 69 | foreach (var library in libraries) 70 | { 71 | // if we are only targetting specific libraries which options.LibraryNames would be null or empty, skip the ones not in the list by matching them to the lower invarient name 72 | if (options.LibraryNames != null && 73 | options.LibraryNames.Count > 0 && 74 | !options.LibraryNames.Any(ln => ln.ToLowerInvariant() == library.LibraryName.ToLowerInvariant())) 75 | { 76 | logger.LogInformation(MessageConstants.LibrarySkipped, library.LibraryName); 77 | continue; 78 | } 79 | 80 | //process the library in batch format starting from 0 81 | var results = await BatchProcessLibrary(client, library, options); 82 | if (results.Success) 83 | { 84 | if (results.RecordsSkipped > 0) 85 | { 86 | logger.LogInformation(MessageConstants.LibraryProcessedSuccessWithSkipped, library.LibraryName, results.RecordsProcessed, results.RecordsSkipped); 87 | } 88 | else 89 | { 90 | logger.LogInformation(MessageConstants.LibraryProcessedSuccess, library.LibraryName, results.RecordsProcessed); 91 | } 92 | } 93 | else 94 | { 95 | logger.LogError(MessageConstants.LibraryItemsNoResults, library.LibraryName, library.LibraryType, library.LibraryId); 96 | } 97 | } 98 | } 99 | catch (Exception ex) 100 | { 101 | logger.LogError(MessageConstants.ExceptionHeaderMessage); 102 | logger.LogError(MessageConstants.ExceptionTypeMessage, ex.GetType().ToString()); 103 | logger.LogError(MessageConstants.ExceptionMessageMessage, ex.Message); 104 | if (ex.InnerException != null) 105 | { 106 | logger.LogError(MessageConstants.ExceptionInnerExceptionTypeMessage, ex.InnerException.GetType().ToString()); 107 | logger.LogError(MessageConstants.ExceptionInnerExceptionMessageMessage, ex.InnerException.Message); 108 | } 109 | logger.LogError(MessageConstants.ExceptionSourceMessage, ex.Source); 110 | logger.LogError(MessageConstants.ExceptionStackTraceMessage, ex.StackTrace); 111 | 112 | return 1; 113 | } 114 | 115 | logger.LogInformation(MessageConstants.CompletedMessage); 116 | return 0; 117 | } 118 | 119 | private async Task BatchProcessLibrary(RestClient client, Library library, GeneratorOptions options, int startingIndex = 0, ProcessingResults carryoverResults = null) 120 | { 121 | var pagingHeaders = new Dictionary 122 | { 123 | { PlexApiConstants.ContainerStart, startingIndex.ToString() }, 124 | { PlexApiConstants.ContainerSize, options.ItemsPerPage.ToString() } 125 | }; 126 | 127 | var itemRoot = await RestClientHelper.CreateAndGetRestResponse( 128 | client, 129 | $"{PlexApiConstants.LibrarySectionsRequestUrl}/{library.LibraryId}/{PlexApiConstants.SearchAll}", 130 | Method.Get, 131 | pagingHeaders); 132 | 133 | var items = itemRoot?.MediaItemContainer?.MediaItems; 134 | 135 | int itemsProcessed = 0; 136 | int itemsSkipped = 0; 137 | 138 | if (items == null && startingIndex == 0) 139 | { 140 | return new ProcessingResults { Success = false, RecordsProcessed = 0 }; 141 | } 142 | else if (items == null) 143 | { 144 | return carryoverResults; 145 | } 146 | 147 | // step through each item in the library and drop a .plexmatch file in it's root 148 | foreach (var item in items) 149 | { 150 | // if we are only targetting specific shows which options.ShowNames would be null or empty, skip the ones not in the list by matching them to the lower invarient mediaitemtitle 151 | if (options.ShowNames != null && 152 | options.ShowNames.Count > 0 && 153 | !options.ShowNames.Any(sn => sn.ToLowerInvariant() == item.MediaItemTitle.ToLowerInvariant())) 154 | { 155 | logger.LogInformation(MessageConstants.ShowSkipped, item.MediaItemTitle); 156 | itemsSkipped++; 157 | continue; 158 | } 159 | else 160 | { 161 | itemsProcessed++; 162 | } 163 | 164 | var locationInfoRoot = await RestClientHelper.CreateAndGetRestResponse(client, $"{PlexApiConstants.MetaDataRequestUrl}/{item.MediaItemId}", Method.Get); 165 | 166 | var locationInfos = locationInfoRoot?.MediaItemInfoContainer?.MediaItemInfos; 167 | 168 | if (locationInfos is null) 169 | { 170 | logger.LogError(MessageConstants.NoLocationInfoForItemFound, item.MediaItemTitle, item.MediaItemId); 171 | continue; 172 | } 173 | 174 | foreach (var locationInfo in locationInfos) 175 | { 176 | List possibleMediaLocations = new List(); 177 | 178 | if (library.LibraryType == PlexApiConstants.MovieLibraryType && locationInfo.MediaInfos != null) 179 | { 180 | possibleMediaLocations = locationInfo.MediaInfos.SelectMany(mi => mi.MediaParts).Select(mp => (IMediaPath)mp).ToList(); 181 | 182 | possibleMediaLocations.ForEach(pml => 183 | { 184 | var lastForwardSlash = pml.MediaItemPath.LastIndexOf("/"); 185 | var lastBackwardSlash = pml.MediaItemPath.LastIndexOf(@"\"); 186 | 187 | pml.MediaItemPath = pml.MediaItemPath.Substring(0, (lastBackwardSlash > lastForwardSlash) ? lastBackwardSlash : lastForwardSlash); 188 | }); 189 | } 190 | else if ((library.LibraryType == PlexApiConstants.TVLibraryType || library.LibraryType == PlexApiConstants.MusicLibraryType) && locationInfo.MediaItemLocations != null) 191 | { 192 | possibleMediaLocations = locationInfo.MediaItemLocations.Select(mil => (IMediaPath)mil).ToList(); 193 | } 194 | else 195 | { 196 | logger.LogWarning(MessageConstants.NoMediaFound, item.MediaItemTitle); 197 | continue; 198 | } 199 | 200 | foreach (var location in possibleMediaLocations) 201 | { 202 | var mediaPath = location.MediaItemPath; 203 | 204 | foreach (var rootPath in options.RootPaths) 205 | { 206 | if (mediaPath.StartsWith(rootPath.PlexRootPath)) 207 | { 208 | mediaPath = mediaPath.Replace(rootPath.PlexRootPath, rootPath.HostRootPath); 209 | break; 210 | } 211 | } 212 | 213 | if (Directory.Exists(mediaPath)) 214 | { 215 | var finalWritePath = Path.Combine(mediaPath, FileConstants.PlexMatchFileName); 216 | 217 | if (options.NoOverwrite && File.Exists(finalWritePath)) 218 | { 219 | logger.LogInformation(MessageConstants.NoWriteBecauseDisabled, item.MediaItemTitle); 220 | continue; 221 | } 222 | 223 | using StreamWriter sw = new StreamWriter(finalWritePath, false); 224 | PlexMatchFileHelper.WritePlexMatchFile(sw, new PlexMatchInfo 225 | { 226 | FileType = PlexMatchFileType.Main, 227 | MediaItemTitle = item.MediaItemTitle, 228 | MediaItemReleaseYear = item.MediaItemReleaseYear, 229 | MediaItemPlexMatchGuid = item.MediaItemPlexMatchGuid 230 | }); 231 | 232 | logger.LogInformation(MessageConstants.PlexMatchWritten, item.MediaItemTitle); 233 | } 234 | else 235 | { 236 | logger.LogError(MessageConstants.FolderMissingOrInvalid, mediaPath); 237 | } 238 | } 239 | 240 | // if per season processing is enable, or this uses non-standard sorting, process the seasons 241 | if (item.MediaType == "show" && (options.EnablePerSeasonProcessing || locationInfo.ShowOrdering != ShowOrdering.Default)) 242 | { 243 | Dictionary seasonPaths = new Dictionary(); 244 | // get the season information 245 | var seasonInfo = await RestClientHelper.CreateAndGetRestResponse( 246 | client, 247 | $"{PlexApiConstants.MetaDataRequestUrl}/{item.MediaItemId}/{PlexApiConstants.MediaItemChildren}", 248 | Method.Get); 249 | 250 | // make sure there is at least 1 season 251 | if(seasonInfo?.MediaItemContainer?.MediaItems?.Any() != true) 252 | { 253 | continue; 254 | } 255 | 256 | // get the season children information so we can extract the season path(s) information 257 | foreach (var season in seasonInfo.MediaItemContainer.MediaItems) 258 | { 259 | var seasonChildrenInfo = await RestClientHelper.CreateAndGetRestResponse( 260 | client, 261 | $"{PlexApiConstants.MetaDataRequestUrl}/{season.MediaItemId}/{PlexApiConstants.MediaItemChildren}", 262 | Method.Get); 263 | 264 | var seasonChildren = seasonChildrenInfo?.MediaItemInfoContainer?.MediaItemInfos; 265 | 266 | if(seasonChildren?.Any() != true) 267 | { 268 | continue; 269 | } 270 | 271 | var uniqueSeasonPaths = seasonChildren 272 | .SelectMany(episode => episode.MediaInfos 273 | .SelectMany(info => info.MediaParts 274 | .Select(part => Path.GetDirectoryName(part.MediaItemPath)))) 275 | .Distinct() 276 | .ToList(); 277 | 278 | foreach (var path in uniqueSeasonPaths) 279 | { 280 | if (!seasonPaths.ContainsKey(path)) 281 | { 282 | seasonPaths.Add(path, new PlexMatchInfo 283 | { 284 | FileType = PlexMatchFileType.Season, 285 | MediaItemTitle = item.MediaItemTitle, 286 | MediaItemReleaseYear = item.MediaItemReleaseYear, 287 | MediaItemPlexMatchGuid = season.MediaItemPlexMatchGuid, //use the season plexmatch guid 288 | SeasonNumber = season.SeasonNumber 289 | }); 290 | } 291 | } 292 | } 293 | 294 | 295 | // write the plexmatch file for the season 296 | foreach (var plexMatchPathAndFile in seasonPaths) 297 | { 298 | var mediaPath = plexMatchPathAndFile.Key; 299 | 300 | foreach (var rootPath in options.RootPaths) 301 | { 302 | // ensure both rootpath and mediapath use the same directory separator 303 | var normalizedRootPath = Path.GetFullPath(rootPath.PlexRootPath); 304 | var normalizedMediaPath = Path.GetFullPath(mediaPath); 305 | 306 | 307 | if (normalizedMediaPath.StartsWith(normalizedRootPath)) 308 | { 309 | mediaPath = normalizedMediaPath.Replace(normalizedRootPath, rootPath.HostRootPath); 310 | break; 311 | } 312 | } 313 | 314 | if (Directory.Exists(mediaPath)) 315 | { 316 | var finalWritePath = Path.Combine(mediaPath, FileConstants.PlexMatchFileName); 317 | 318 | if (options.NoOverwrite && File.Exists(finalWritePath)) 319 | { 320 | logger.LogInformation(MessageConstants.NoWriteBecauseDisabled, item.MediaItemTitle); 321 | continue; 322 | } 323 | 324 | using StreamWriter sw = new StreamWriter(finalWritePath, false); 325 | PlexMatchFileHelper.WritePlexMatchFile(sw, plexMatchPathAndFile.Value); 326 | logger.LogInformation(MessageConstants.PlexMatchSeasonWritten, item.MediaItemTitle, plexMatchPathAndFile.Value.SeasonNumber, plexMatchPathAndFile.Key); 327 | } 328 | else 329 | { 330 | logger.LogError(MessageConstants.FolderMissingOrInvalid, mediaPath); 331 | } 332 | } 333 | } 334 | } 335 | } 336 | 337 | if (carryoverResults == null) 338 | { 339 | carryoverResults = new ProcessingResults { Success = true }; 340 | } 341 | 342 | carryoverResults.RecordsProcessed += itemsProcessed; 343 | carryoverResults.RecordsSkipped += itemsSkipped; 344 | 345 | return await BatchProcessLibrary(client, library, options, startingIndex + options.ItemsPerPage, carryoverResults); 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /PlexMatchGenerator/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using PlexMatchGenerator.Services; 3 | 4 | namespace PlexMatchGenerator 5 | { 6 | public class Startup 7 | { 8 | public Startup() 9 | { 10 | } 11 | 12 | public void ConfigureServices(IServiceCollection services) 13 | { 14 | services.AddSingleton(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlexMatch File Generator 2 | 3 | This application generates a .plexmatch file in the directory of all shows and movies added to your Plex Server. This is especially useful for migrating storage devices if you have some shows that needed a custom match. 4 | 5 | ## Usage 6 | 7 | The command is expecting 2 arguments from the command line 8 | 9 | - Plex Server Token (-t or --token) 10 | - Plex Server Url (-u or --url) 11 | 12 | For information on how to get your Plex token, see this support link: [https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) 13 | 14 | As of release 0.9.2-rc2 the default behavior for TV Show processing is to perform per season processing when episode ordering is set to anything other than library default. This action preserves the episode ordering as currently set. 15 | 16 | ### Optional Parameters 17 | 18 | - Modify root path (-r or --root) 19 | - Use this option to set the root path used to be different than what your Plex server returns. For instance if your Plex container has the media mounted to /media and the computer running the application has it mounted to /mnt/media 20 | - Usage: -r /path/on/host:/path/on/plex 21 | - Set log path (-l or --log) 22 | - This will output the log to file at the path specified, log file will be named `plexmatch.log` in the directory specified. The path specified must exist! If this option is not specified, no log file will be output. 23 | - Usage: -l /home/user/logs 24 | - Per season processing of TV Shows (-sp or --seasonprocessing) 25 | - This will process each season of each TV Show individually writing a plexmatch file for both the show as a whole and each season 26 | - This behavior is the default for any show with the episode sorting set to anything other than the library default 27 | - Usage: -sp 28 | - Restrict to specific libraries (-lib or --library) 29 | - This parameter can be specified multiple times 30 | - Each library specified is added to the allow list 31 | - If even 1 library is added, every library not matching will be ignored 32 | - Library names are **not** case-sensitive 33 | - Usage: -lib TV 34 | - Usage: -lib "TV Shows" 35 | - Restrict to specific media items (-s or --show) 36 | - Despite the parameter name, this can be used on any library and is not restricted to TV Shows 37 | - This works best when specified with -lib as otherwise this will have a significant performance impact on larger libraries 38 | - This parameter can be specified multiple times 39 | - Each media item specified is added to the allow list 40 | - If even 1 media item is added, every media item not matching will be ignored 41 | - Media item names are **not** case-sensitive 42 | - Usage: -s firefly 43 | - Get version (--version) 44 | - This will output the version of the executable being run 45 | - Disable Automatic Overwrite (--nooverwrite or -no) 46 | - This will skip writing any file if it already exists instead of overwriting it. 47 | - Set page size for batch processing (--pagesize or -ps) 48 | - This will change the default batch processing size. Default is 20 items 49 | - Usage: -ps 10 50 | - View help (--help) 51 | - This will output the parameter help 52 | 53 | ### Examples 54 | 55 | `./PlexMatchGenerator-Linux-x64 -u http://192.168.0.3:32400 -t ABCD12345` 56 | 57 | `./PlexMatchGenerator-Linux-x64 --url http://192.168.0.3:32400 --token ABCD12345` 58 | 59 | `PlexMatchGenerator-Windows-x86.exe -u http://192.168.0.3:32400 -t ABCD12345` 60 | 61 | `PlexMatchGenerator-Windows-x86.exe --url http://192.168.0.3:32400 --token ABCD12345` 62 | 63 | ## Donations 64 | 65 | Donations are always accepted but never required. Currently I accept PayPal using the button below. 66 | 67 | [![Paypal Donation Image Button](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate/?business=XPYMV5XQG8JCN&no_recurring=0¤cy_code=USD) 68 | 69 | ## 3rd Party Packages Used 70 | 71 | - [RestSharp](https://restsharp.dev/) 72 | - [Newtonsoft.Json](https://www.newtonsoft.com/json) 73 | - [Serilog](https://serilog.net/) 74 | 75 | ## Development Environment Setup 76 | 77 | - Clone repo 78 | - Open folder with VS Code 79 | - Ensure C# support is added (it should prompt if not) 80 | - Restore Nuget packages (command line: dotnet restore) 81 | - Application should build immediately out of box 82 | 83 | ## Contributing 84 | 85 | Contributions are welcome! 86 | 87 | - Fork the repo 88 | - Make your change 89 | - Submit a Pull Request, Ensure to include details of what problem is fixed, what is improved, etc. If it is in response to an issue, tag the issue on the PR 90 | - Submit your PR to merge into the Develop branch, merge requests to the main branch will be denied 91 | - Respond to and/or make changes based on PR comments 92 | --------------------------------------------------------------------------------