From 29586667cb500dc46fff1a75ec482c022df75140 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sat, 14 Jan 2017 01:03:37 +0100 Subject: [PATCH] Files tab is now present. (#245) * Adding file info tab. Not finished yet. * Adding more media info options. * Deleting files now works. Fixes #127 * Fix button for modifying episode files. * Get Media Info when running DiskScanService. --- src/NzbDrone.Api/Movies/MovieFileModule.cs | 89 +++++++++++++++ src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + src/NzbDrone.Api/Series/MovieFileResource.cs | 7 +- .../Migration/117_update_movie_file.cs | 45 ++++++++ .../EpisodeImport/ImportApprovedMovie.cs | 1 + .../EpisodeImport/ImportDecisionMaker.cs | 2 +- .../MediaFiles/MediaFileService.cs | 5 + src/NzbDrone.Core/MediaFiles/MovieFile.cs | 1 + src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/UI/Cells/FileTitleCell.js | 15 +++ src/UI/Cells/MediaInfoCell.js | 23 ++++ src/UI/Cells/QualityCellTemplate.hbs | 4 +- src/UI/Cells/ReleaseTitleCell.js | 2 +- src/UI/Movies/Details/MoviesDetailsLayout.js | 30 ++++- .../Movies/Details/MoviesDetailsTemplate.hbs | 4 +- src/UI/Movies/Files/DeleteFileCell.js | 26 +++++ src/UI/Movies/Files/FileModel.js | 3 + src/UI/Movies/Files/FilesCollection.js | 30 +++++ src/UI/Movies/Files/FilesLayout.js | 107 ++++++++++++++++++ src/UI/Movies/Files/FilesLayoutTemplate.hbs | 1 + src/UI/Movies/Files/NoFilesView.js | 5 + src/UI/Movies/Files/NoFilesViewTemplate.hbs | 3 + 22 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 src/NzbDrone.Api/Movies/MovieFileModule.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/117_update_movie_file.cs create mode 100644 src/UI/Cells/FileTitleCell.js create mode 100644 src/UI/Cells/MediaInfoCell.js create mode 100644 src/UI/Movies/Files/DeleteFileCell.js create mode 100644 src/UI/Movies/Files/FileModel.js create mode 100644 src/UI/Movies/Files/FilesCollection.js create mode 100644 src/UI/Movies/Files/FilesLayout.js create mode 100644 src/UI/Movies/Files/FilesLayoutTemplate.hbs create mode 100644 src/UI/Movies/Files/NoFilesView.js create mode 100644 src/UI/Movies/Files/NoFilesViewTemplate.hbs diff --git a/src/NzbDrone.Api/Movies/MovieFileModule.cs b/src/NzbDrone.Api/Movies/MovieFileModule.cs new file mode 100644 index 000000000..a45fbefad --- /dev/null +++ b/src/NzbDrone.Api/Movies/MovieFileModule.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.IO; +using NLog; +using NzbDrone.Api.REST; +using NzbDrone.Api.Movie; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.SignalR; + +namespace NzbDrone.Api.EpisodeFiles +{ + public class MovieFileModule : NzbDroneRestModuleWithSignalR + //IHandle + { + private readonly IMediaFileService _mediaFileService; + private readonly IRecycleBinProvider _recycleBinProvider; + private readonly IMovieService _seriesService; + private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly Logger _logger; + + public MovieFileModule(IBroadcastSignalRMessage signalRBroadcaster, + IMediaFileService mediaFileService, + IRecycleBinProvider recycleBinProvider, + IMovieService seriesService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + Logger logger) + : base(signalRBroadcaster) + { + _mediaFileService = mediaFileService; + _recycleBinProvider = recycleBinProvider; + _seriesService = seriesService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + _logger = logger; + /*GetResourceById = GetEpisodeFile; + GetResourceAll = GetEpisodeFiles; + UpdateResource = SetQuality;*/ + DeleteResource = DeleteEpisodeFile; + } + + /*private EpisodeFileResource GetEpisodeFile(int id) + { + var episodeFile = _mediaFileService.Get(id); + var series = _seriesService.GetSeries(episodeFile.SeriesId); + + return episodeFile.ToResource(series, _qualityUpgradableSpecification); + } + + private List GetEpisodeFiles() + { + if (!Request.Query.SeriesId.HasValue) + { + throw new BadRequestException("seriesId is missing"); + } + + var seriesId = (int)Request.Query.SeriesId; + + var series = _seriesService.GetSeries(seriesId); + + return _mediaFileService.GetFilesBySeries(seriesId).ConvertAll(f => f.ToResource(series, _qualityUpgradableSpecification)); + } + + private void SetQuality(EpisodeFileResource episodeFileResource) + { + var episodeFile = _mediaFileService.Get(episodeFileResource.Id); + episodeFile.Quality = episodeFileResource.Quality; + _mediaFileService.Update(episodeFile); + }*/ + + private void DeleteEpisodeFile(int id) + { + var episodeFile = _mediaFileService.GetMovie(id); + var series = _seriesService.GetMovie(episodeFile.MovieId); + var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); + + _logger.Info("Deleting episode file: {0}", fullPath); + _recycleBinProvider.DeleteFile(fullPath); + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); + } + + public void Handle(EpisodeFileAddedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.Id); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index f92541404..6a61d84fc 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -116,6 +116,7 @@ + diff --git a/src/NzbDrone.Api/Series/MovieFileResource.cs b/src/NzbDrone.Api/Series/MovieFileResource.cs index 62ad7832f..848d31ab4 100644 --- a/src/NzbDrone.Api/Series/MovieFileResource.cs +++ b/src/NzbDrone.Api/Series/MovieFileResource.cs @@ -30,8 +30,8 @@ namespace NzbDrone.Api.Movie public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } public MovieResource Movie { get; set; } - - + public string Edition { get; set; } + public Core.MediaFiles.MediaInfo.MediaInfoModel MediaInfo { get; set; } //TODO: Add series statistics as a property of the series (instead of individual properties) } @@ -63,7 +63,8 @@ namespace NzbDrone.Api.Movie ReleaseGroup = model.ReleaseGroup, Quality = model.Quality, Movie = movie, - + MediaInfo = model.MediaInfo, + Edition = model.Edition }; } diff --git a/src/NzbDrone.Core/Datastore/Migration/117_update_movie_file.cs b/src/NzbDrone.Core/Datastore/Migration/117_update_movie_file.cs new file mode 100644 index 000000000..8e45441c1 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/117_update_movie_file.cs @@ -0,0 +1,45 @@ +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(117)] + public class update_movie_file : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.Column("Edition").OnTable("MovieFiles").AsString().Nullable(); + Execute.WithConnection(SetSortTitles); + } + + private void SetSortTitles(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand getSeriesCmd = conn.CreateCommand()) + { + getSeriesCmd.Transaction = tran; + getSeriesCmd.CommandText = @"SELECT Id, RelativePath FROM MovieFiles"; + using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) + { + while (seriesReader.Read()) + { + var id = seriesReader.GetInt32(0); + var relativePath = seriesReader.GetString(1); + + var edition = Parser.Parser.ParseMovieTitle(relativePath).Edition; + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE MovieFiles SET Edition = ? WHERE Id = ?"; + updateCmd.AddParameter(edition); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs index 128ca64f9..1a2c812d6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs @@ -83,6 +83,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.MediaInfo = localMovie.MediaInfo; episodeFile.Movie = localMovie.Movie; episodeFile.ReleaseGroup = localMovie.ParsedMovieInfo.ReleaseGroup; + episodeFile.Edition = localMovie.ParsedMovieInfo.Edition; bool copyOnly; switch (importMode) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 9d4abe042..080ef2399 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public List GetImportDecisions(List videoFiles, Movie movie) { - return GetImportDecisions(videoFiles, movie, null, false); + return GetImportDecisions(videoFiles, movie, null, true); } public List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource) diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index 0587bb793..37e663ee5 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles List FilterExistingFiles(List files, Series series); List FilterExistingFiles(List files, Movie movie); EpisodeFile Get(int id); + MovieFile GetMovie(int id); List Get(IEnumerable ids); List GetMovies(IEnumerable ids); @@ -150,5 +151,9 @@ namespace NzbDrone.Core.MediaFiles _movieFileRepository.Update(episodeFile); } + public MovieFile GetMovie(int id) + { + return _movieFileRepository.Get(id); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MovieFile.cs b/src/NzbDrone.Core/MediaFiles/MovieFile.cs index dfb753ab6..9bb0f1ddd 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFile.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFile.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaFiles public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } public MediaInfoModel MediaInfo { get; set; } + public string Edition { get; set; } public LazyLoaded Movie { get; set; } public override string ToString() diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index a0221be03..7e70c9898 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -183,6 +183,7 @@ + diff --git a/src/UI/Cells/FileTitleCell.js b/src/UI/Cells/FileTitleCell.js new file mode 100644 index 000000000..372ee07c4 --- /dev/null +++ b/src/UI/Cells/FileTitleCell.js @@ -0,0 +1,15 @@ +var NzbDroneCell = require('./NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'file-title-cell', + + render : function() { + this.$el.empty(); + + var title = this.model.get('relativePath'); + this.$el.html(title); + + + return this; + } +}); diff --git a/src/UI/Cells/MediaInfoCell.js b/src/UI/Cells/MediaInfoCell.js new file mode 100644 index 000000000..ed42380a3 --- /dev/null +++ b/src/UI/Cells/MediaInfoCell.js @@ -0,0 +1,23 @@ +var NzbDroneCell = require('./NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'release-title-cell', + + render : function() { + this.$el.empty(); + + var info = this.model.get('mediaInfo'); + if (info) { + var runtime = info.runTime; + if (runtime) { + runtime = runtime.split(".")[0]; + } + var video = "{0} ({1}x{2}) ({3})".format(info.videoCodec, info.width, info.height, runtime); + var audio = "{0} ({1})".format(info.audioFormat, info.audioLanguages); + this.$el.html(video + " " + audio); + } + + + return this; + } +}); diff --git a/src/UI/Cells/QualityCellTemplate.hbs b/src/UI/Cells/QualityCellTemplate.hbs index 6625ade9b..9c76376a9 100644 --- a/src/UI/Cells/QualityCellTemplate.hbs +++ b/src/UI/Cells/QualityCellTemplate.hbs @@ -1,5 +1,5 @@ {{#if_gt proper compare="1"}} {{quality.name}} {{else}} - {{quality.name}} -{{/if_gt}} \ No newline at end of file + {{quality.name}} +{{/if_gt}} diff --git a/src/UI/Cells/ReleaseTitleCell.js b/src/UI/Cells/ReleaseTitleCell.js index 7d3551e41..761c642ff 100644 --- a/src/UI/Cells/ReleaseTitleCell.js +++ b/src/UI/Cells/ReleaseTitleCell.js @@ -17,4 +17,4 @@ module.exports = NzbDroneCell.extend({ return this; } -}); \ No newline at end of file +}); diff --git a/src/UI/Movies/Details/MoviesDetailsLayout.js b/src/UI/Movies/Details/MoviesDetailsLayout.js index da92d260a..1d01f8439 100644 --- a/src/UI/Movies/Details/MoviesDetailsLayout.js +++ b/src/UI/Movies/Details/MoviesDetailsLayout.js @@ -11,6 +11,7 @@ var LoadingView = require('../../Shared/LoadingView'); var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); var HistoryLayout = require('../History/MovieHistoryLayout'); var SearchLayout = require('../Search/MovieSearchLayout'); +var FilesLayout = require("../Files/FilesLayout"); require('backstrech'); require('../../Mixins/backbone.signalr.mixin'); @@ -22,7 +23,8 @@ module.exports = Marionette.Layout.extend({ seasons : '#seasons', info : '#info', search : '#movie-search', - history : '#movie-history' + history : '#movie-history', + files : "#movie-files" }, @@ -36,11 +38,12 @@ module.exports = Marionette.Layout.extend({ poster : '.x-movie-poster', manualSearch : '.x-manual-search', history : '.x-movie-history', - search : '.x-movie-search' + search : '.x-movie-search', + files : ".x-movie-files" }, events : { - 'click .x-episode-file-editor' : '_openEpisodeFileEditor', + 'click .x-episode-file-editor' : '_showFiles', 'click .x-monitored' : '_toggleMonitored', 'click .x-edit' : '_editMovie', 'click .x-refresh' : '_refreshMovies', @@ -48,7 +51,8 @@ module.exports = Marionette.Layout.extend({ 'click .x-search' : '_moviesSearch', 'click .x-manual-search' : '_showSearch', 'click .x-movie-history' : '_showHistory', - 'click .x-movie-search' : '_showSearch' + 'click .x-movie-search' : '_showSearch', + "click .x-movie-files" : "_showFiles", }, initialize : function() { @@ -72,11 +76,18 @@ module.exports = Marionette.Layout.extend({ this.searchLayout = new SearchLayout({ model : this.model }); this.searchLayout.startManualSearch = true; + this.filesLayout = new FilesLayout({ model : this.model }); + this._showBackdrop(); this._showSeasons(); this._setMonitoredState(); this._showInfo(); - this._showHistory(); + if (this.model.get("movieFile")) { + this._showFiles() + } else { + this._showHistory(); + } + }, onRender : function() { @@ -144,6 +155,15 @@ module.exports = Marionette.Layout.extend({ this.search.show(this.searchLayout); }, + _showFiles : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.files.tab('show'); + this.files.show(this.filesLayout); + }, + _toggleMonitored : function() { var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); diff --git a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs index 8516e5665..8e5bb874e 100644 --- a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs +++ b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs @@ -9,7 +9,7 @@ {{title}}
- +
@@ -42,10 +42,12 @@
diff --git a/src/UI/Movies/Files/DeleteFileCell.js b/src/UI/Movies/Files/DeleteFileCell.js new file mode 100644 index 000000000..45f815f04 --- /dev/null +++ b/src/UI/Movies/Files/DeleteFileCell.js @@ -0,0 +1,26 @@ +var vent = require('vent'); +var Backgrid = require('backgrid'); + +module.exports = Backgrid.Cell.extend({ + className : 'delete-episode-file-cell', + + events : { + 'click' : '_onClick' + }, + + render : function() { + this.$el.empty(); + this.$el.html(''); + + return this; + }, + + _onClick : function() { + var self = this; + if (window.confirm('Are you sure you want to delete \'{0}\' from disk?'.format(this.model.get('relativePath')))) { + this.model.destroy().done(function() { + vent.trigger(vent.Events.MovieFileDeleted, { movieFile : self.model }); + }); + } + } +}); diff --git a/src/UI/Movies/Files/FileModel.js b/src/UI/Movies/Files/FileModel.js new file mode 100644 index 000000000..cb2f217f3 --- /dev/null +++ b/src/UI/Movies/Files/FileModel.js @@ -0,0 +1,3 @@ +var Backbone = require('backbone'); + +module.exports = Backbone.Model.extend({}); diff --git a/src/UI/Movies/Files/FilesCollection.js b/src/UI/Movies/Files/FilesCollection.js new file mode 100644 index 000000000..dc787838e --- /dev/null +++ b/src/UI/Movies/Files/FilesCollection.js @@ -0,0 +1,30 @@ +var PagableCollection = require('backbone.pageable'); +var FileModel = require('./FileModel'); +var AsSortedCollection = require('../../Mixins/AsSortedCollection'); + +var Collection = PagableCollection.extend({ + url : window.NzbDrone.ApiRoot + "/moviefile", + model : FileModel, + + state : { + pageSize : 2000, + sortKey : 'title', + order : -1 + }, + + mode : 'client', + + sortMappings : { + 'quality' : { + sortKey : "qualityWeight" + }, + "edition" : { + sortKey : "edition" + } + }, + +}); + +Collection = AsSortedCollection.call(Collection); + +module.exports = Collection; diff --git a/src/UI/Movies/Files/FilesLayout.js b/src/UI/Movies/Files/FilesLayout.js new file mode 100644 index 000000000..3e6dd2bdd --- /dev/null +++ b/src/UI/Movies/Files/FilesLayout.js @@ -0,0 +1,107 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +//var ButtonsView = require('./ButtonsView'); +//var ManualSearchLayout = require('./ManualLayout'); +var FilesCollection = require('./FilesCollection'); +var CommandController = require('../../Commands/CommandController'); +var LoadingView = require('../../Shared/LoadingView'); +var NoResultsView = require('./NoFilesView'); +var FileModel = require("./FileModel"); +var FileTitleCell = require('../../Cells/FileTitleCell'); +var FileSizeCell = require('../../Cells/FileSizeCell'); +var QualityCell = require('../../Cells/QualityCell'); +var MediaInfoCell = require('../../Cells/MediaInfoCell'); +var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell'); +var DownloadReportCell = require('../../Release/DownloadReportCell'); +var AgeCell = require('../../Release/AgeCell'); +var ProtocolCell = require('../../Release/ProtocolCell'); +var PeersCell = require('../../Release/PeersCell'); +var EditionCell = require('../../Cells/EditionCell'); +var DeleteFileCell = require("./DeleteFileCell"); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Files/FilesLayoutTemplate', + + regions : { + main : '#movie-files-region', + grid : "#movie-files-grid" + }, + + events : { + 'click .x-search-auto' : '_searchAuto', + 'click .x-search-manual' : '_searchManual', + 'click .x-search-back' : '_showButtons' + }, + + columns : [ + { + name : 'title', + label : 'Title', + cell : FileTitleCell + }, + { + name : "mediaInfo", + label : "Media Info", + cell : MediaInfoCell + }, + { + name : 'edition', + label : 'Edition', + cell : EditionCell, + title : "Edition", + }, + { + name : 'size', + label : 'Size', + cell : FileSizeCell + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell, + }, + { + name : "delete", + label : "", + cell : DeleteFileCell, + } + ], + + + initialize : function(movie) { + this.filesCollection = new FilesCollection(); + var file = movie.model.get("movieFile"); + this.filesCollection.add(file); + //this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); + }, + + onShow : function() { + this.grid.show(new Backgrid.Grid({ + row : Backgrid.Row, + columns : this.columns, + collection : this.filesCollection, + className : 'table table-hover' + })); + }, + + _showMainView : function() { + this.main.show(this.mainView); + }, + + _showButtons : function() { + this._showMainView(); + }, + + _showSearchResults : function() { + if (this.releaseCollection.length === 0) { + this.mainView = new NoResultsView(); + } + + else { + //this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); + } + + this._showMainView(); + } +}); diff --git a/src/UI/Movies/Files/FilesLayoutTemplate.hbs b/src/UI/Movies/Files/FilesLayoutTemplate.hbs new file mode 100644 index 000000000..ac6a3ca36 --- /dev/null +++ b/src/UI/Movies/Files/FilesLayoutTemplate.hbs @@ -0,0 +1 @@ +
diff --git a/src/UI/Movies/Files/NoFilesView.js b/src/UI/Movies/Files/NoFilesView.js new file mode 100644 index 000000000..22a2f7c4c --- /dev/null +++ b/src/UI/Movies/Files/NoFilesView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Files/NoFilesViewTemplate' +}); diff --git a/src/UI/Movies/Files/NoFilesViewTemplate.hbs b/src/UI/Movies/Files/NoFilesViewTemplate.hbs new file mode 100644 index 000000000..300e4f666 --- /dev/null +++ b/src/UI/Movies/Files/NoFilesViewTemplate.hbs @@ -0,0 +1,3 @@ +

+ No files for this movie. +