From 07fbd2027ff5bad0e06ee7e6ae5abd841ae9812d Mon Sep 17 00:00:00 2001 From: softworkz Date: Thu, 26 May 2016 03:27:32 +0200 Subject: [PATCH] New MovieOrganizer PlugIn --- MediaBrowser.Plugins.sln | 8 + MovieOrganizer/Api/MovieOrganizerApi.cs | 54 +++ .../Configuration/PluginConfiguration.cs | 12 + MovieOrganizer/Html/fileorganizer.js | 411 ++++++++++++++++++ .../Html/fileorganizer.template.html | 91 ++++ MovieOrganizer/Html/metadataviewer.js | 138 ++++++ .../Html/metadataviewer.template.html | 211 +++++++++ MovieOrganizer/HtmlHelper.cs | 38 ++ MovieOrganizer/Images/thumb.jpg | Bin 0 -> 61215 bytes MovieOrganizer/MovieOrganizer.csproj | 117 +++++ MovieOrganizer/Plugin.cs | 51 +++ MovieOrganizer/Properties/AssemblyInfo.cs | 31 ++ MovieOrganizer/ServerEntryPoint.cs | 148 +++++++ MovieOrganizer/Service/FileOrganizerBase.cs | 163 +++++++ .../Service/MovieFileOrganizationRequest.cs | 13 + MovieOrganizer/Service/MovieFileOrganizer.cs | 223 ++++++++++ .../Service/MovieOrganizerService.cs | 84 ++++ MovieOrganizer/packages.config | 9 + 18 files changed, 1802 insertions(+) create mode 100644 MovieOrganizer/Api/MovieOrganizerApi.cs create mode 100644 MovieOrganizer/Configuration/PluginConfiguration.cs create mode 100644 MovieOrganizer/Html/fileorganizer.js create mode 100644 MovieOrganizer/Html/fileorganizer.template.html create mode 100644 MovieOrganizer/Html/metadataviewer.js create mode 100644 MovieOrganizer/Html/metadataviewer.template.html create mode 100644 MovieOrganizer/HtmlHelper.cs create mode 100644 MovieOrganizer/Images/thumb.jpg create mode 100644 MovieOrganizer/MovieOrganizer.csproj create mode 100644 MovieOrganizer/Plugin.cs create mode 100644 MovieOrganizer/Properties/AssemblyInfo.cs create mode 100644 MovieOrganizer/ServerEntryPoint.cs create mode 100644 MovieOrganizer/Service/FileOrganizerBase.cs create mode 100644 MovieOrganizer/Service/MovieFileOrganizationRequest.cs create mode 100644 MovieOrganizer/Service/MovieFileOrganizer.cs create mode 100644 MovieOrganizer/Service/MovieOrganizerService.cs create mode 100644 MovieOrganizer/packages.config diff --git a/MediaBrowser.Plugins.sln b/MediaBrowser.Plugins.sln index 9694dfd7..55f20260 100644 --- a/MediaBrowser.Plugins.sln +++ b/MediaBrowser.Plugins.sln @@ -54,6 +54,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetadataViewer", "MetadataV EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.ADEProvider", "MediaBrowser.Plugins.ADEProvider\MediaBrowser.Plugins.ADEProvider.csproj", "{2F1E5734-05C5-495D-B11B-42C4762CF4CF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MovieOrganizer", "MovieOrganizer\MovieOrganizer.csproj", "{0449243E-8FBD-47DF-8E79-8FBA6E735D36}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -200,6 +202,12 @@ Global {2F1E5734-05C5-495D-B11B-42C4762CF4CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F1E5734-05C5-495D-B11B-42C4762CF4CF}.Release|Any CPU.Build.0 = Release|Any CPU {2F1E5734-05C5-495D-B11B-42C4762CF4CF}.Release|x86.ActiveCfg = Release|Any CPU + {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Debug|x86.ActiveCfg = Debug|Any CPU + {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Release|Any CPU.Build.0 = Release|Any CPU + {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MovieOrganizer/Api/MovieOrganizerApi.cs b/MovieOrganizer/Api/MovieOrganizerApi.cs new file mode 100644 index 00000000..5208e529 --- /dev/null +++ b/MovieOrganizer/Api/MovieOrganizerApi.cs @@ -0,0 +1,54 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Logging; +using MovieOrganizer.Service; +using ServiceStack; +using System.Threading; + +namespace MovieOrganizer.Api +{ + [Route("/Library/FileOrganizations/{Id}/Movie/OrganizeExt", "POST", Summary = "Performs organization of a movie file")] + public class OrganizeMovie + { + [ApiMember(Name = "Id", Description = "Result Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + + [ApiMember(Name = "MovieName", Description = "Movie Name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string MovieName { get; set; } + + [ApiMember(Name = "MovieYear", Description = "Movie Year", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string MovieYear { get; set; } + + [ApiMember(Name = "TargetFolder", Description = "Target Folder", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string TargetFolder { get; set; } + } + + [Authenticated] + public class MovieOrganizerApi : IRestfulService + { + private readonly ILogger _logger; + private readonly MovieOrganizerService _service; + private readonly ILibraryManager _libraryManager; + + public MovieOrganizerApi(ILogManager logManager, MovieOrganizerService service, ILibraryManager libraryManager) + { + _logger = logManager.GetLogger(GetType().Name); + _service = service; + _libraryManager = libraryManager; + } + + public void Post(OrganizeMovie request) + { + var task = _service.PerformMovieOrganization(new MovieFileOrganizationRequest + { + ResultId = request.Id, + Name = request.MovieName, + Year = request.MovieYear, + TargetFolder = request.TargetFolder + }); + + // Wait 2s for exceptions that may occur and would be automatically forwarded to the client for immediate error display + task.Wait(2000); + } + } +} diff --git a/MovieOrganizer/Configuration/PluginConfiguration.cs b/MovieOrganizer/Configuration/PluginConfiguration.cs new file mode 100644 index 00000000..891536f1 --- /dev/null +++ b/MovieOrganizer/Configuration/PluginConfiguration.cs @@ -0,0 +1,12 @@ +using MediaBrowser.Model.Plugins; +using System; + +namespace MovieOrganizer.Configuration +{ + public class PluginConfiguration : BasePluginConfiguration + { + public PluginConfiguration() + { + } + } +} diff --git a/MovieOrganizer/Html/fileorganizer.js b/MovieOrganizer/Html/fileorganizer.js new file mode 100644 index 00000000..0e60d367 --- /dev/null +++ b/MovieOrganizer/Html/fileorganizer.js @@ -0,0 +1,411 @@ +define(['dialogHelper', 'paper-checkbox', 'paper-input', 'paper-button', 'paper-icon-button-light', 'paper-tabs'], function (dialogHelper) { + + var extractedName; + var extractedYear; + var currentNewItem; + var existingSeriesHtml; + var seriesLocationsCount = 0; + var movieLocationsCount = 0; + + function onApiFailure(e) { + + Dashboard.hideLoadingMsg(); + + require(['alert'], function (alert) { + alert({ + title: Globalize.translate('AutoOrganizeError'), + text: Globalize.translate('ErrorOrganizingFileWithErrorCode', e.headers.get("X-Application-Error-Code")) + }); + }); + } + + function initForms(context, item) { + + if (!item.ExtractedName || item.ExtractedName.length < 3) { + context.querySelector('.fldRemember').classList.add('hide'); + } + else { + context.querySelector('.fldRemember').classList.remove('hide'); + } + + context.querySelector('.inputFile').innerHTML = item.OriginalFileName; + context.querySelector('.inputSeriesName').innerHTML = item.ExtractedName; + context.querySelector('.inputYear').innerHTML = item.ExtractedYear; + + context.querySelector('.inputFile2').innerHTML = item.OriginalFileName; + context.querySelector('.inputSeriesName2').innerHTML = item.ExtractedName; + context.querySelector('.inputYear2').innerHTML = item.ExtractedYear; + + if (!item.ExtractedName) { + context.querySelector('.paraInputSeriesName').classList.add('hide'); + context.querySelector('.paraInputSeriesName2').classList.add('hide'); + } + + if (!item.ExtractedYear) { + context.querySelector('.paraInputYear').classList.add('hide'); + context.querySelector('.paraInputYear2').classList.add('hide'); + } + + context.querySelector('#txtSeason').value = item.ExtractedSeasonNumber; + context.querySelector('#txtEpisode').value = item.ExtractedEpisodeNumber; + context.querySelector('#txtEndingEpisode').value = item.ExtractedEndingEpisodeNumber; + context.querySelector('.extractedName').innerHTML = item.ExtractedName; + + extractedName = item.ExtractedName; + extractedYear = item.ExtractedYear; + + context.querySelector('#chkRememberCorrection').checked = false; + + context.querySelector('#hfResultId').value = item.Id; + context.querySelector('#hfResultIdMovie').value = item.Id; + + ApiClient.getItems(null, { + recursive: true, + includeItemTypes: 'Series', + sortBy: 'SortName' + + }).then(function (result) { + + existingSeriesHtml = result.Items.map(function (s) { + + return ''; + + }).join(''); + + existingSeriesHtml = '' + existingSeriesHtml; + + context.querySelector('#selectSeries').innerHTML = existingSeriesHtml; + + ApiClient.getVirtualFolders().then(function (result) { + + var movieLocations = []; + var seriesLocations = []; + + for (var n = 0; n < result.length; n++) { + + var virtualFolder = result[n]; + + for (var i = 0, length = virtualFolder.Locations.length; i < length; i++) { + var location = { + value: virtualFolder.Locations[i], + display: virtualFolder.Name + ': ' + virtualFolder.Locations[i] + }; + + if (virtualFolder.CollectionType == 'movies') { + movieLocations.push(location); + } + + if (virtualFolder.CollectionType == 'tvshows') { + seriesLocations.push(location); + } + } + } + + seriesLocationsCount = seriesLocations.length; + movieLocationsCount = movieLocations.length; + + var seriesFolderHtml = seriesLocations.map(function (s) { + return ''; + }).join(''); + + var movieFolderHtml = movieLocations.map(function (s) { + return ''; + }).join(''); + + if (seriesLocations.length > 1) { + // If the user has multiple folders, add an empty item to enforce a manual selection + seriesFolderHtml = '' + seriesFolderHtml; + } + + if (movieLocations.length > 1) { + // If the user has multiple folders, add an empty item to enforce a manual selection + movieFolderHtml = '' + movieFolderHtml; + } + + context.querySelector('#selectSeriesFolder').innerHTML = seriesFolderHtml; + context.querySelector('#selectMovieFolder').innerHTML = movieFolderHtml; + + context.querySelector('#paperTabs').selectIndex(0); + + }, onApiFailure); + + }, onApiFailure); + } + + function submitEpisodeForm(dlg) { + + Dashboard.showLoadingMsg(); + + var resultId = dlg.querySelector('#hfResultId').value; + var seriesId = dlg.querySelector('#selectSeries').value; + + var targetFolder; + var newProviderIds; + var newSeriesName; + var newSeriesYear; + + if (seriesId == "##NEW##" && currentNewItem != null) { + seriesId = null; + newProviderIds = JSON.stringify(currentNewItem.ProviderIds); + newSeriesName = currentNewItem.Name; + newSeriesYear = currentNewItem.ProductionYear; + targetFolder = dlg.querySelector('#selectSeriesFolder').value; + } + + var options = { + + SeriesId: seriesId, + SeasonNumber: dlg.querySelector('#txtSeason').value, + EpisodeNumber: dlg.querySelector('#txtEpisode').value, + EndingEpisodeNumber: dlg.querySelector('#txtEndingEpisode').value, + RememberCorrection: dlg.querySelector('#chkRememberCorrection').checked, + NewSeriesProviderIds: newProviderIds, + NewSeriesName: newSeriesName, + NewSeriesYear: newSeriesYear, + TargetFolder: targetFolder + }; + + ApiClient.performEpisodeOrganization(resultId, options).then(function () { + + Dashboard.hideLoadingMsg(); + + dlg.submitted = true; + dialogHelper.close(dlg); + + }, onApiFailure); + } + + function submitMovieForm(dlg) { + + var targetFolder = dlg.querySelector('#selectMovieFolder').value; + + if (!targetFolder) { + require(['alert'], function (alert) { + alert({ + title: "Target folder", + text: "Please select target folder!" + }); + }); + + return; + } + + Dashboard.showLoadingMsg(); + + var resultId = dlg.querySelector('#hfResultIdMovie').value; + + + var options = { + + MovieName: currentNewItem.Name, + MovieYear: currentNewItem.ProductionYear, + TargetFolder: targetFolder + }; + + performMovieOrganization(resultId, options).then(function () { + + Dashboard.hideLoadingMsg(); + + dlg.submitted = true; + dialogHelper.close(dlg); + + }, onApiFailure); + } + + function performMovieOrganization(id, options) { + + var url = ApiClient.getUrl("Library/FileOrganizations/" + id + "/Movie/OrganizeExt"); + + return ApiClient.ajax({ + type: "POST", + url: url, + data: JSON.stringify(options), + contentType: 'application/json' + }); + }; + + function showNewSeriesDialog(dlg) { + + if (seriesLocationsCount == 0) { + + require(['alert'], function (alert) { + alert({ + title: Globalize.translate('AutoOrganizeError'), + text: Globalize.translate('NoTvFoldersConfigured') + }); + }); + return; + } + + require(['components/itemidentifier/itemidentifier'], function (itemidentifier) { + + itemidentifier.showFindNew(extractedName, extractedYear, 'Series').then(function (newItem) { + + if (newItem != null) { + currentNewItem = newItem; + var seriesHtml = existingSeriesHtml; + seriesHtml = seriesHtml + ''; + dlg.querySelector('#selectSeries').innerHTML = seriesHtml; + selectedSeriesChanged(dlg); + } + }); + }); + } + + function showFindMovieDialog(dlg) { + + if (movieLocationsCount == 0) { + + require(['alert'], function (alert) { + alert({ + title: Globalize.translate('AutoOrganizeError'), + text: Globalize.translate('NoTvFoldersConfigured') + }); + }); + return; + } + + require(['components/itemidentifier/itemidentifier'], function (itemidentifier) { + + itemidentifier.showFindNew(extractedName, extractedYear, 'Movie').then(function (newItem) { + + if (newItem != null) { + currentNewItem = newItem; + var movieName = currentNewItem.Name; + + if (currentNewItem.ProductionYear) + { + movieName = movieName + ' (' + currentNewItem.ProductionYear + ')'; + } + + dlg.querySelector('#txtSelectedMovie').value = movieName; + dlg.querySelector('#identifiedMovie').classList.remove('hide'); + + } + }); + }); + } + + function selectedSeriesChanged(dlg) { + var seriesId = dlg.querySelector('#selectSeries').value; + + if (seriesId == "##NEW##") { + dlg.querySelector('.fldSelectSeriesFolder').classList.remove('hide'); + dlg.querySelector('#selectSeriesFolder').setAttribute('required', 'required'); + } + else { + dlg.querySelector('.fldSelectSeriesFolder').classList.add('hide'); + dlg.querySelector('#selectSeriesFolder').removeAttribute('required'); + } + } + + function selectTab(dlg, tabIndex) { + + if (tabIndex == 0) { + dlg.querySelector('#organizeSeries').classList.remove('hide'); + dlg.querySelector('#organizeMovies').classList.add('hide'); + } + else { + dlg.querySelector('#organizeSeries').classList.add('hide'); + dlg.querySelector('#organizeMovies').classList.remove('hide'); + } + } + + + return { + show: function (item) { + return new Promise(function (resolve, reject) { + + extractedName = null; + extractedYear = null; + currentNewItem = null; + existingSeriesHtml = null; + + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'components/fileorganizer/fileorganizer.template.html', true); + + xhr.onload = function (e) { + + var template = this.response; + var dlg = dialogHelper.createDialog({ + removeOnClose: true, + size: 'small' + }); + + dlg.classList.add('ui-body-a'); + dlg.classList.add('background-theme-a'); + + dlg.classList.add('formDialog'); + + var html = ''; + + html += Globalize.translateDocument(template); + + dlg.innerHTML = html; + document.body.appendChild(dlg); + + dlg.querySelector('.dialogHeaderTitle').innerHTML = Globalize.translate('FileOrganizeManually'); + + dialogHelper.open(dlg); + + dlg.addEventListener('close', function () { + + if (dlg.submitted) { + resolve(); + } else { + reject(); + } + }); + + dlg.querySelector('.btnCancel').addEventListener('click', function (e) { + + dialogHelper.close(dlg); + }); + + dlg.querySelector('.episodeCorrectionForm').addEventListener('submit', function (e) { + + submitEpisodeForm(dlg); + + e.preventDefault(); + return false; + }); + + dlg.querySelector('.organizeMovieForm').addEventListener('submit', function (e) { + + submitMovieForm(dlg); + + e.preventDefault(); + return false; + }); + + dlg.querySelector('#btnNewSeries').addEventListener('click', function (e) { + + showNewSeriesDialog(dlg); + }); + + dlg.querySelector('#btnIdentifyMovie').addEventListener('click', function (e) { + + showFindMovieDialog(dlg); + }); + + dlg.querySelector('#selectSeries').addEventListener('change', function (e) { + + selectedSeriesChanged(dlg); + }); + + dlg.querySelector('#paperTabs').addEventListener('iron-select', function (e) { + + var tabs = dlg.querySelector('#paperTabs'); + selectTab(dlg, tabs.selected); + }); + + selectTab(dlg, 0); + + initForms(dlg, item); + } + + xhr.send(); + }); + } + }; +}); \ No newline at end of file diff --git a/MovieOrganizer/Html/fileorganizer.template.html b/MovieOrganizer/Html/fileorganizer.template.html new file mode 100644 index 00000000..03ad55d4 --- /dev/null +++ b/MovieOrganizer/Html/fileorganizer.template.html @@ -0,0 +1,91 @@ +
+ +
+
+
+ + + +
Series Episode
+
+ + +
Organize Movie
+
+
+ +
+
+
+ +

Filename:

+

Extracted Series Name:

+

Extracted Year::

+ +
+
+ + +
+ +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
${LabelEndingEpisodeNumberHelp}
+
+
+
+ ${OptionRememberOrganizeCorrection} +
+
+ + +
+
+ +
+
+ +

Filename:

+

Extracted Name:

+

Extracted Year::

+ +
+ +
+ +
+
+ +
+
+
+
+ + +
+
+ +
+ +
+
+
diff --git a/MovieOrganizer/Html/metadataviewer.js b/MovieOrganizer/Html/metadataviewer.js new file mode 100644 index 00000000..6d22a745 --- /dev/null +++ b/MovieOrganizer/Html/metadataviewer.js @@ -0,0 +1,138 @@ +define(['dialogHelper', 'paper-fab', 'paper-input', 'paper-checkbox', 'detailtablecss'], function (paperDialogHelper) { + + var currentItem; + var currentItemType; + var currentDeferred; + var currentSearchResult; + + function showMetadataTable(page, item) { + + Dashboard.showLoadingMsg(); + + var lang = page.querySelector('#selectLanguage').value; + + ApiClient.getJSON(ApiClient.getUrl('Items/' + item.Id + '/MetadataRaw', { language: lang })).then(function (table) { + + var htmlLookup = ''; + + for (var i = 0; i < table.LookupData.length; i++) { + + var row = table.LookupData[i]; + if (row.Key == 'MetadataLanguage') { + + page.querySelector('#selectLanguage').value = row.Value || ''; + } + else { + + htmlLookup += ''; + htmlLookup += ''; + } + } + + htmlLookup += '
' + row.Key + '' + row.Value + '
'; + page.querySelector('#searchCriteria').innerHTML = htmlLookup; + + var html = ''; + html += ''; + } + + html += ''; + html += ''; + + for (var i = 0; i < table.Rows.length; i++) { + + var row = table.Rows[i]; + html += ''; + + for (var n = 0; n < row.Values.length; n++) { + html += ''; + } + + html += ''; + } + + html += '
'; + + for (var i = 0; i < table.Headers.length; i++) { + html += '' + table.Headers[i] + '
' + row.Caption + '' + (row.Values[n] == null ? '' : row.Values[n]) + '
'; + + page.querySelector('.metadataRawTable').innerHTML = html; + + Dashboard.hideLoadingMsg(); + }); + } + + function onDialogClosed() { + + Dashboard.hideLoadingMsg(); + currentDeferred.resolveWith(null, [hasChanges]); + } + + function showEditor(itemId) { + + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'components/metadataviewer/metadataviewer.template.html', true); + + xhr.onload = function (e) { + + var template = this.response; + + ApiClient.getItem(Dashboard.getCurrentUserId(), itemId).then(function (item) { + + var dlg = paperDialogHelper.createDialog({ + size: 'large' + }); + + dlg.classList.add('ui-body-b'); + dlg.classList.add('background-theme-b'); + dlg.classList.add('popupEditor'); + + var html = ''; + + html += '

'; + html += ''; + html += '
' + item.Name + '
'; + html += '

'; + + html += '
'; + html += Globalize.translateDocument(template); + html += '
'; + + dlg.innerHTML = html; + document.body.appendChild(dlg); + + paperDialogHelper.open(dlg); + + dlg.querySelector('.btnCloseDialog').addEventListener('click', function (e) { + + paperDialogHelper.close(dlg); + }); + + dlg.querySelector('#selectLanguage').addEventListener('change', function (e) { + + showMetadataTable(dlg, item); + }); + + dlg.addEventListener('iron-overlay-closed', function () { + + Dashboard.hideLoadingMsg(); + }); + + dlg.classList.add('metadataViewer'); + + showMetadataTable(dlg, item); + }); + } + + xhr.send(); + } + + return { + show: function (itemId) { + return new Promise(function (resolve, reject) { + + showEditor(itemId); + }); + } + }; +}); \ No newline at end of file diff --git a/MovieOrganizer/Html/metadataviewer.template.html b/MovieOrganizer/Html/metadataviewer.template.html new file mode 100644 index 00000000..80052c08 --- /dev/null +++ b/MovieOrganizer/Html/metadataviewer.template.html @@ -0,0 +1,211 @@ +
+

Lookup Criteria:

+
+
+
+ + + + + +
+ + + +
+
+
+ +

+

Results:

+
+

+

+

diff --git a/MovieOrganizer/HtmlHelper.cs b/MovieOrganizer/HtmlHelper.cs new file mode 100644 index 00000000..71608a94 --- /dev/null +++ b/MovieOrganizer/HtmlHelper.cs @@ -0,0 +1,38 @@ +using MediaBrowser.Common.Configuration; +using MovieOrganizer.Configuration; +using System.IO; +using System.Reflection; +using System.Text; + +namespace MovieOrganizer +{ + internal static class HtmlHelper + { + public static MemoryStream OrganizerScript { get; private set; } + + public static MemoryStream OrganizerTemplate { get; private set; } + + + + public static void InstallFiles(IApplicationPaths appPaths, PluginConfiguration config) + { + OrganizerScript = GetResourceStream("fileorganizer.js"); + OrganizerTemplate = GetResourceStream("fileorganizer.template.html"); + } + + public static void UninstallFiles(IApplicationPaths appPaths, PluginConfiguration config) + { + OrganizerScript = null; + OrganizerTemplate = null; + } + + private static MemoryStream GetResourceStream(string resourceName) + { + var fullQualName = "MovieOrganizer.Html." + resourceName; + var stream = typeof(HtmlHelper).GetTypeInfo().Assembly.GetManifestResourceStream(fullQualName); + var memStream = new MemoryStream((int)stream.Length); + stream.CopyTo(memStream); + return memStream; + } + } +} diff --git a/MovieOrganizer/Images/thumb.jpg b/MovieOrganizer/Images/thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dd0f8d7b0da342425d09e24dc06264357dab06a4 GIT binary patch literal 61215 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 afKCNkjg0m2|14Vk|KtC6&xh#0 + + + + Debug + AnyCPU + {0449243E-8FBD-47DF-8E79-8FBA6E735D36} + Library + Properties + MovieOrganizer + MovieOrganizer + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\CommonIO.1.0.0.9\lib\net45\CommonIO.dll + True + + + ..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll + True + + + ..\packages\MediaBrowser.Common.3.0.647\lib\net45\MediaBrowser.Common.dll + True + + + ..\packages\MediaBrowser.Server.Core.3.0.647\lib\net45\MediaBrowser.Controller.dll + True + + + ..\packages\MediaBrowser.Common.3.0.647\lib\net45\MediaBrowser.Model.dll + True + + + ..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll + + + ..\..\Emby\ThirdParty\ServiceStack\ServiceStack.dll + + + ..\packages\ServiceStack.Interfaces.4.0.35\lib\portable-wp80+sl5+net40+win8+monotouch+monoandroid\ServiceStack.Interfaces.dll + + + + + + + + + + + + + Properties\SharedVersion.cs + + + + + + + + + + + + + + + + + + + + + + + + + + xcopy "$(TargetPath)" "$(SolutionDir)\..\MediaBrowser.Dev\ProgramData-Server\Plugins\" /y + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/MovieOrganizer/Plugin.cs b/MovieOrganizer/Plugin.cs new file mode 100644 index 00000000..75c32edb --- /dev/null +++ b/MovieOrganizer/Plugin.cs @@ -0,0 +1,51 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Serialization; +using MovieOrganizer.Configuration; + +namespace MovieOrganizer +{ + public class Plugin : BasePlugin + { + IApplicationPaths _appPaths; + + public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) + : base(appPaths, xmlSerializer) + { + Instance = this; + _appPaths = appPaths; + HtmlHelper.InstallFiles(_appPaths, PluginConfiguration); + } + + public override string Name + { + get { return StaticName; } + } + + public static string StaticName + { + get { return "Movie Organizer"; } + } + + public override string Description + { + get + { + return "Allows organizing movies from the Auto-Organize dialog"; + } + } + + public override void OnUninstalling() + { + HtmlHelper.UninstallFiles(_appPaths, PluginConfiguration); + base.OnUninstalling(); + } + + public static Plugin Instance { get; private set; } + + public PluginConfiguration PluginConfiguration + { + get { return Configuration; } + } + } +} diff --git a/MovieOrganizer/Properties/AssemblyInfo.cs b/MovieOrganizer/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..54f2e4ae --- /dev/null +++ b/MovieOrganizer/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MovieOrganizer Beta 1")] +[assembly: AssemblyDescription("Beta 1")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MovieOrganizer")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6AE8B402-796A-4898-96AC-7817D8F12D48")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// \ No newline at end of file diff --git a/MovieOrganizer/ServerEntryPoint.cs b/MovieOrganizer/ServerEntryPoint.cs new file mode 100644 index 00000000..49cf2383 --- /dev/null +++ b/MovieOrganizer/ServerEntryPoint.cs @@ -0,0 +1,148 @@ +using CommonIO; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using MovieOrganizer; +using MovieOrganizer.Api; +using MovieOrganizer.Service; +using ServiceStack; +using ServiceStack.Host.Handlers; +using ServiceStack.Web; +using System.IO; +using System.Web; + +namespace MovieOrganizer +{ + /// + /// All communication between the server and the plugins server instance should occur in this class. + /// + public class ServerEntryPoint : IServerEntryPoint + { + private readonly ILibraryManager _libraryManager; + private readonly ILibraryMonitor _libraryMonitor; + private readonly ILogger _logger; + private readonly IServerApplicationHost _appHost; + private readonly IFileSystem _fileSystem; + private readonly IHttpServer _httpServer; + private readonly IServerConfigurationManager _configurationManager; + private readonly ILocalizationManager _localizationManager; + private readonly IServerManager _serverManager; + private readonly IProviderManager _providerManager; + private readonly IFileOrganizationService _fileOrganizationService; + + private MovieOrganizerApi _api; + private MovieOrganizerService _service; + + public static ServerEntryPoint Instance { get; private set; } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public ServerEntryPoint( + IServerConfigurationManager configurationManager, + ILibraryManager libraryManager, + ILibraryMonitor libraryMonitor, + ILogManager logger, + IServerApplicationHost appHost, + IHttpServer httpServer, + IFileSystem fileSystem, + ILocalizationManager localizationManager, + IServerManager serverManager, + IProviderManager providerManager, + IFileOrganizationService fileOrganizationService) + { + Instance = this; + _libraryManager = libraryManager; + _libraryMonitor = libraryMonitor; + _logger = logger.GetLogger("MovieOrganizer"); + _appHost = appHost; + _fileSystem = fileSystem; + _configurationManager = configurationManager; + _httpServer = httpServer; + _localizationManager = localizationManager; + _serverManager = serverManager; + _providerManager = providerManager; + _fileOrganizationService = fileOrganizationService; + + var serviceStackHost = (IAppHost)httpServer; + serviceStackHost.RawHttpHandlers.Add(ProcessRequestRaw); + + _service = new MovieOrganizerService( + _configurationManager, + logger, + _fileSystem, + _appHost, + _libraryMonitor, + _libraryManager, + _localizationManager, + _serverManager, + _providerManager, + _fileOrganizationService); + + _api = new MovieOrganizerApi(logger, _service, _libraryManager); + } + + /// + /// + /// + public void Run() + { + } + + /// + /// + /// + public void Dispose() + { + } + + public virtual HttpAsyncTaskHandler ProcessRequestRaw(IHttpRequest request) + { + MemoryStream resultStream = null; + + if (request.PathInfo.Contains("/components/fileorganizer/fileorganizer.js")) + { + resultStream = HtmlHelper.OrganizerScript; + } + else if (request.PathInfo.Contains("/components/fileorganizer/fileorganizer.template.html")) + { + resultStream = HtmlHelper.OrganizerTemplate; + } + + if (resultStream != null) + { + var handler = new CustomActionHandler((httpReq, httpRes) => + { + httpRes.ContentType = "text/html"; + + lock (resultStream) + { + resultStream.Seek(0, SeekOrigin.Begin); + resultStream.WriteTo(httpRes.OutputStream); + httpRes.EndRequest(); + } + + httpRes.End(); + }); + + return handler; + } + + return null; + } + } +} \ No newline at end of file diff --git a/MovieOrganizer/Service/FileOrganizerBase.cs b/MovieOrganizer/Service/FileOrganizerBase.cs new file mode 100644 index 00000000..ac6a30f4 --- /dev/null +++ b/MovieOrganizer/Service/FileOrganizerBase.cs @@ -0,0 +1,163 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Localization; + +namespace MovieOrganizer.Service +{ + public abstract class FileOrganizerBase + { + protected readonly ILibraryMonitor _libraryMonitor; + protected readonly ILibraryManager _libraryManager; + protected readonly ILogger _logger; + protected readonly IFileSystem _fileSystem; + protected readonly IFileOrganizationService _organizationService; + protected readonly IServerConfigurationManager _config; + protected readonly IProviderManager _providerManager; + protected readonly IServerManager _serverManager; + protected readonly ILocalizationManager _localizationManager; + + protected readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public FileOrganizerBase(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IServerManager serverManager, ILocalizationManager localizationManager) + { + _organizationService = organizationService; + _config = config; + _fileSystem = fileSystem; + _logger = logger; + _libraryManager = libraryManager; + _libraryMonitor = libraryMonitor; + _providerManager = providerManager; + _serverManager = serverManager; + _localizationManager = localizationManager; + } + + public static string GetTitleAsSearchTerm(string title) + { + const string ReplaceChars = "!\"§$%&/\\(){}[]=?´`°^'#+-/*@,.-;:_<>|"; + if (title != null) + { + foreach (Char c in ReplaceChars) + { + title = title.Replace(c, ' '); + } + } + + title = title.Replace(" ", " "); + title = title.Replace(" ", " "); + + return title; + } + + public abstract Task OrganizeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken); + + public abstract Task OrganizeWithCorrection(MovieFileOrganizationRequest request, AutoOrganizeOptions options, CancellationToken cancellationToken); + + /// + /// Deletes leftover files and empty folders if configured to do so. + /// + /// + /// + /// + public void DeleteLeftoverFilesAndEmptyFolders(AutoOrganizeOptions options, string folderPath) + { + var deleteExtensions = options.TvOptions.LeftOverFileExtensionsToDelete + .Select(i => i.Trim().TrimStart('.')) + .Where(i => !string.IsNullOrEmpty(i)) + .Select(i => "." + i) + .ToList(); + + if (deleteExtensions.Count > 0) + { + DeleteLeftOverFiles(folderPath, deleteExtensions); + } + + if (options.TvOptions.DeleteEmptyFolders) + { + if (!IsWatchFolder(folderPath, options.TvOptions.WatchLocations)) + { + DeleteEmptyFolders(folderPath); + } + } + } + + /// + /// Deletes the left over files. + /// + /// The path. + /// The extensions. + private void DeleteLeftOverFiles(string path, IEnumerable extensions) + { + var eligibleFiles = _fileSystem.GetFiles(path, true) + .Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + foreach (var file in eligibleFiles) + { + try + { + _fileSystem.DeleteFile(file.FullName); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting file {0}", ex, file.FullName); + } + } + } + + /// + /// Deletes the empty folders. + /// + /// The path. + private void DeleteEmptyFolders(string path) + { + try + { + foreach (var d in _fileSystem.GetDirectoryPaths(path)) + { + DeleteEmptyFolders(d); + } + + var entries = _fileSystem.GetFileSystemEntryPaths(path); + + if (!entries.Any()) + { + try + { + _logger.Debug("Deleting empty directory {0}", path); + _fileSystem.DeleteDirectory(path, false); + } + catch (UnauthorizedAccessException) { } + catch (DirectoryNotFoundException) { } + } + } + catch (UnauthorizedAccessException) { } + } + + /// + /// Determines if a given folder path is contained in a folder list + /// + /// The folder path to check. + /// A list of folders. + private bool IsWatchFolder(string path, IEnumerable watchLocations) + { + return watchLocations.Contains(path, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MovieOrganizer/Service/MovieFileOrganizationRequest.cs b/MovieOrganizer/Service/MovieFileOrganizationRequest.cs new file mode 100644 index 00000000..add512ea --- /dev/null +++ b/MovieOrganizer/Service/MovieFileOrganizationRequest.cs @@ -0,0 +1,13 @@ +namespace MovieOrganizer.Service +{ + public class MovieFileOrganizationRequest + { + public string ResultId { get; set; } + + public string Name { get; set; } + + public string Year { get; set; } + + public string TargetFolder { get; set; } + } +} \ No newline at end of file diff --git a/MovieOrganizer/Service/MovieFileOrganizer.cs b/MovieOrganizer/Service/MovieFileOrganizer.cs new file mode 100644 index 00000000..50cb6845 --- /dev/null +++ b/MovieOrganizer/Service/MovieFileOrganizer.cs @@ -0,0 +1,223 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Localization; + +namespace MovieOrganizer.Service +{ + public class MovieFileOrganizer : FileOrganizerBase + { + public MovieFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IServerManager serverManager, ILocalizationManager localizationManager) : + base(organizationService, config, fileSystem, logger, libraryManager, libraryMonitor, providerManager, serverManager, localizationManager) + { + } + + public override Task OrganizeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken) + { + throw new NotImplementedException("Auto-Organize is not implemented for movie files at this time."); + } + + public override async Task OrganizeWithCorrection(MovieFileOrganizationRequest baseRequest, AutoOrganizeOptions options, CancellationToken cancellationToken) + { + var request = (MovieFileOrganizationRequest)baseRequest; + + var result = _organizationService.GetResult(request.ResultId); + + var file = _fileSystem.GetFileInfo(result.OriginalPath); + + result.Type = FileOrganizerType.Movie; + + await OrganizeMovie(result.OriginalPath, request.Name, request.Year, request.TargetFolder, options, true, result, cancellationToken).ConfigureAwait(false); + + await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); + + if (file != null && file.Exists && file.DirectoryName != null) + { + this.DeleteLeftoverFilesAndEmptyFolders(options, file.DirectoryName); + } + + return result; + } + + private async Task OrganizeMovie(string sourcePath, string movieName, string movieYear, string targetPath, AutoOrganizeOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken) + { + _logger.Info("Sorting file {0} into movie folder {1}", sourcePath, targetPath); + + // Proceed to sort the file + var newPath = GetNewPath(sourcePath, movieName, movieYear, targetPath, options, true, result, cancellationToken); + + if (string.IsNullOrEmpty(newPath)) + { + var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + return; + } + + _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); + result.TargetPath = newPath; + + var fileExists = _fileSystem.FileExists(result.TargetPath); + + if (!overwriteExisting) + { + if (options.TvOptions.CopyOriginalFile && fileExists) + { + _logger.Info("File {0} already copied to new path {1}, stopping organization", sourcePath, newPath); + result.Status = FileSortingStatus.SkippedExisting; + result.StatusMessage = string.Empty; + return; + } + + if (fileExists) + { + result.Status = FileSortingStatus.SkippedExisting; + result.StatusMessage = string.Empty; + return; + } + } + + PerformFileSorting(options.TvOptions, result); + + ////if (overwriteExisting) + ////{ + //// var hasRenamedFiles = false; + + //// foreach (var path in otherDuplicatePaths) + //// { + //// _logger.Debug("Removing duplicate episode {0}", path); + + //// _libraryMonitor.ReportFileSystemChangeBeginning(path); + + //// var renameRelatedFiles = !hasRenamedFiles && + //// string.Equals(Path.GetDirectoryName(path), Path.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase); + + //// if (renameRelatedFiles) + //// { + //// hasRenamedFiles = true; + //// } + + //// try + //// { + //// DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath); + //// } + //// catch (IOException ex) + //// { + //// _logger.ErrorException("Error removing duplicate episode", ex, path); + //// } + //// finally + //// { + //// _libraryMonitor.ReportFileSystemChangeComplete(path, true); + //// } + //// } + ////} + } + + private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) + { + try + { + _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); + + var targetAlreadyExists = _fileSystem.FileExists(result.TargetPath); + + try + { + if (targetAlreadyExists || options.CopyOriginalFile) + { + _fileSystem.CopyFile(result.OriginalPath, result.TargetPath, true); + } + else + { + _fileSystem.MoveFile(result.OriginalPath, result.TargetPath); + } + + result.Status = FileSortingStatus.Success; + result.StatusMessage = string.Empty; + } + catch (Exception ex) + { + var errorMsg = string.Format("Failed to move file from {0} to {1}", result.OriginalPath, result.TargetPath); + + result.Status = FileSortingStatus.Failure; + result.StatusMessage = errorMsg; + _logger.ErrorException(errorMsg, ex); + + return; + } + finally + { + _libraryMonitor.ReportFileSystemChangeComplete(result.TargetPath, true); + } + + if (targetAlreadyExists && !options.CopyOriginalFile) + { + try + { + _fileSystem.DeleteFile(result.OriginalPath); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); + } + } + } + finally + { + if (_serverManager != null) + { + _serverManager.SendWebSocketMessageAsync("AutoOrganizeUpdate", () => string.Empty, CancellationToken.None); + } + } + } + + /// + /// Gets the new path. + /// + /// The source path. + /// The series. + /// The season number. + /// The episode number. + /// The ending episode number. + /// The options. + /// System.String. + private string GetNewPath(string sourcePath, string movieName, string movieYear, string targetPath, AutoOrganizeOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken) + { + var folderName = _fileSystem.GetValidFilename(movieName).Trim(); + + if (!string.IsNullOrEmpty(movieYear)) + { + folderName = string.Format("{0} ({1})", folderName, movieYear); + } + + var newPath = Path.Combine(targetPath, folderName); + + var fileName = _fileSystem.GetFileNameWithoutExtension(sourcePath); + fileName = string.Format("{0}{1}", fileName, Path.GetExtension(sourcePath)); + + newPath = Path.Combine(newPath, fileName); + + return newPath; + } + + } +} diff --git a/MovieOrganizer/Service/MovieOrganizerService.cs b/MovieOrganizer/Service/MovieOrganizerService.cs new file mode 100644 index 00000000..ebe394ac --- /dev/null +++ b/MovieOrganizer/Service/MovieOrganizerService.cs @@ -0,0 +1,84 @@ +using CommonIO; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace MovieOrganizer.Service +{ + public class MovieOrganizerService + { + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + private readonly ILibraryMonitor _libraryMonitor; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localizationManager; + private readonly IServerManager _serverManager; + private readonly IProviderManager _providerManager; + private readonly IFileOrganizationService _fileOrganizationService; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + /// The configuration manager. + /// The directory watchers. + /// The log manager. + /// The file system. + public MovieOrganizerService( + IServerConfigurationManager config, + ILogManager logManager, + IFileSystem fileSystem, + IServerApplicationHost appHost, + ILibraryMonitor libraryMonitor, + ILibraryManager libraryManager, + ILocalizationManager localizationManager, + IServerManager serverManager, + IProviderManager providerManager, + IFileOrganizationService fileOrganizationService) + { + _logger = logManager.GetLogger("MovieOrganizer"); + _config = config; + _fileSystem = fileSystem; + _libraryMonitor = libraryMonitor; + _libraryManager = libraryManager; + _localizationManager = localizationManager; + _serverManager = serverManager; + _providerManager = providerManager; + _fileOrganizationService = fileOrganizationService; + } + + public async Task PerformMovieOrganization(MovieFileOrganizationRequest request) + { + // The OrganizeWithCorrection function is not purely async. To workaround this, use .Yield() to immediately return to the caller + //await Task.Yield(); + + var organizer = new MovieFileOrganizer(_fileOrganizationService, _config, _fileSystem, _logger, _libraryManager, + _libraryMonitor, _providerManager, _serverManager, _localizationManager); + + await organizer.OrganizeWithCorrection(request, GetAutoOrganizeOptions(), CancellationToken.None).ConfigureAwait(false); ; + } + + private AutoOrganizeOptions GetAutoOrganizeOptions() + { + return (AutoOrganizeOptions)_config.GetConfiguration("autoorganize"); + } + + } +} diff --git a/MovieOrganizer/packages.config b/MovieOrganizer/packages.config new file mode 100644 index 00000000..ecf00b72 --- /dev/null +++ b/MovieOrganizer/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file