Exemplo n.º 1
0
        public override ItemList ProcessMovie(MovieConfiguration mc, FileInfo file, bool forceRefresh)
        {
            if (!TVSettings.Instance.FolderJpg)
            {
                return(new ItemList());
            }

            FileInfo fi = FileHelper.FileInFolder(file.Directory, DEFAULT_FILE_NAME);
            bool     fileDoesntExist = !doneFolderJpg.Contains(fi.FullName) && !fi.Exists;

            if (!forceRefresh && !fileDoesntExist)
            {
                return(new ItemList());
            }

            CachedMovieInfo?cachedMovie = mc.CachedMovie;

            if (cachedMovie is null)
            {
                return(new ItemList());
            }

            ItemList theActionList = new ItemList();

            //default to poster when we want season posters for the season specific folders
            string downloadPath = cachedMovie.PosterUrl;

            if (!string.IsNullOrEmpty(downloadPath))
            {
                theActionList.Add(new ActionDownloadImage(mc, null, fi, downloadPath));
            }

            doneFolderJpg.Add(fi.FullName);
            return(theActionList);
        }
Exemplo n.º 2
0
        public override ItemList?ProcessMovie(MovieConfiguration si, FileInfo filo, bool forceRefresh)
        {
            //We only want to do something if the fanart option is enabled. If the KODI option is enabled then let it do the work.
            if (TVSettings.Instance.FanArtJpg && !TVSettings.Instance.KODIImages)
            {
                ItemList theActionList = new ItemList();
                foreach (string location in si.Locations)
                {
                    FileInfo fi = FileHelper.FileInFolder(location, DEFAULT_FILE_NAME);

                    bool doesntExist = !fi.Exists;
                    if ((forceRefresh || doesntExist) && !DoneFanartJpg.Contains(fi.FullName))
                    {
                        string bannerPath = si.CachedMovie?.PosterUrl;

                        if (!string.IsNullOrEmpty(bannerPath))
                        {
                            theActionList.Add(new ActionDownloadImage(si, null, fi, bannerPath, false));
                        }

                        DoneFanartJpg.Add(fi.FullName);
                    }
                }

                return(theActionList);
            }

            return(base.ProcessMovie(si, filo, forceRefresh));
        }
Exemplo n.º 3
0
        private static bool MovieNeeded(MovieConfiguration si, DirFilesCache dfc, FileInfo fi)
        {
            if (fi is null)
            {
                throw new ArgumentNullException(nameof(fi));
            }

            if (si is null)
            {
                throw new ArgumentNullException(nameof(si));
            }

            foreach (FileInfo testFileInfo in FindMovieOnDisk(dfc, si))
            {
                //We will check that the file that is found is not the one we are testing
                if (fi.FullName == testFileInfo.FullName)
                {
                    continue;
                }

                //We have found another file that matches
                return(false);
            }
            return(true);
        }
Exemplo n.º 4
0
        private void AddNewMovieToLibrary(PossibleNewMovie ai, bool isInLibraryFolderFileFinder, string?matchingRoot)
        {
            // need to add a new showitem
            MovieConfiguration found = new MovieConfiguration(ai);

            mDoc.FilmLibrary.Add(found);

            mDoc.Stats().AutoAddedMovies++;

            bool inDefaultPath = ai.Directory.Name.Equals(
                CustomMovieName.NameFor(found, TVSettings.Instance.MovieFolderFormat),
                StringComparison.CurrentCultureIgnoreCase);

            if (inDefaultPath && isInLibraryFolderFileFinder)
            {
                found.UseAutomaticFolders       = true;
                found.UseCustomFolderNameFormat = false;
                found.AutomaticFolderRoot       = matchingRoot !;
                found.UseManualLocations        = false;
                return;
            }

            if (isInLibraryFolderFileFinder)
            {
                found.AutomaticFolderRoot = matchingRoot !;
            }

            found.UseAutomaticFolders       = false;
            found.UseCustomFolderNameFormat = false;
            found.UseManualLocations        = true;
            found.ManualLocations.Add(ai.Directory.FullName);
        }
Exemplo n.º 5
0
 public ActionDeleteFile(FileInfo remove, MovieConfiguration mov, TVSettings.TidySettings?tidyup)
 {
     Tidyup      = tidyup;
     PercentDone = 0;
     Movie       = mov;
     toRemove    = remove;
 }
Exemplo n.º 6
0
        public override ItemList?ProcessMovie(MovieConfiguration mc, FileInfo file, bool forceRefresh)
        {
            if (!TVSettings.Instance.NFOMovies || mc.CachedMovie is null)
            {
                return(null);
            }

            FileInfo nfo = FileHelper.FileInFolder(file.Directory, file.MovieFileNameBase() + ".nfo");

            if ((nfo.Exists && System.Math.Abs(mc.CachedMovie.SrvLastUpdated - TimeZoneHelper.Epoch(nfo.LastWriteTime)) < 1 && !forceRefresh))
            {
                return(null);
            }

            //If we do not already have plans to put the file into place
            if (DoneNfo.Contains(nfo.FullName))
            {
                return(null);
            }

            DoneNfo.Add(nfo.FullName);
            return(new ItemList {
                new ActionNfoMovie(nfo, mc)
            });
        }
Exemplo n.º 7
0
 public MovieItemMissing([NotNull] MovieConfiguration movie, [NotNull] string whereItShouldBeFolder)
 {
     Episode      = null;
     Filename     = TVSettings.Instance.FilenameFriendly(CustomMovieName.NameFor(movie, TVSettings.Instance.MovieFilenameFormat));
     TheFileNoExt = whereItShouldBeFolder + System.IO.Path.DirectorySeparatorChar + Filename;
     Folder       = whereItShouldBeFolder;
     Movie        = movie;
 }
Exemplo n.º 8
0
 public ActionDeleteDirectory(DirectoryInfo remove, MovieConfiguration mi, TVSettings.TidySettings tidyup)
 {
     Tidyup      = tidyup;
     PercentDone = 0;
     Episode     = null;
     Movie       = mi;
     toRemove    = remove;
 }
Exemplo n.º 9
0
 public void CheckIfActive(MovieConfiguration si, DirFilesCache dfc, TVDoc.ScanSettings settings)
 {
     if (Active())
     {
         Check(si, dfc, settings);
         LogActionListSummary();
     }
 }
Exemplo n.º 10
0
        private static Action SetupDirectoryRemoval([NotNull] DirectoryInfo di,
                                                    [NotNull] IReadOnlyList <MovieConfiguration> matchingMovies)
        {
            MovieConfiguration si = matchingMovies[0]; //Choose the first cachedSeries

            LOGGER.Info($"Removing {di.FullName} as it matches {si.ShowName} and no files are needed");

            return(new ActionDeleteDirectory(di, si, TVSettings.Instance.Tidyup));
        }
Exemplo n.º 11
0
        public AutoAddMedia(string hint, FileInfo file, bool assumeMovie)
        {
            InitializeComponent();
            ShowConfiguration  = new ShowConfiguration();
            MovieConfiguration = new MovieConfiguration();

            this.assumeMovie = assumeMovie;

            lblFileName.Text = "Filename: " + file.FullName;


            tvCodeFinder = new CombinedCodeFinder("", MediaConfiguration.MediaType.tv, TVDoc.ProviderType.libraryDefault)
            {
                Dock = DockStyle.Fill
            };
            movieCodeFinder = new CombinedCodeFinder("", MediaConfiguration.MediaType.movie, TVDoc.ProviderType.libraryDefault)
            {
                Dock = DockStyle.Fill
            };

            tvCodeFinder.SelectionChanged    += MTCCF_SelectionChanged;
            movieCodeFinder.SelectionChanged += MTCCF_SelectionChanged;

            SingleTvShowFound = tvCodeFinder.SetHint(hint, TVSettings.Instance.DefaultProvider) && TVSettings.Instance.DefShowAutoFolders && TVSettings.Instance.DefShowUseDefLocation;
            SingleMovieFound  = movieCodeFinder.SetHint(hint, TVSettings.Instance.DefaultMovieProvider) && TVSettings.Instance.DefMovieDefaultLocation.HasValue() && TVSettings.Instance.DefMovieUseDefaultLocation && assumeMovie;

            originalHint = hint;

            if (SingleTvShowFound)
            {
                string filenameFriendly = TVSettings.Instance.FilenameFriendly(FileHelper.MakeValidPath(tvCodeFinder.TvShowInitialFound.Name));
                SetShowItem(tvCodeFinder.TvShowInitialFoundCode, tvCodeFinder.Source, TVSettings.Instance.DefShowLocation + System.IO.Path.DirectorySeparatorChar + filenameFriendly);
                if (ShowConfiguration.Code == -1)
                {
                    SetShowItem();
                }
            }
            if (SingleMovieFound)
            {
                SetMovieItem(movieCodeFinder.MovieInitialFoundCode, movieCodeFinder.Source, TVSettings.Instance.DefMovieDefaultLocation);
                if (MovieConfiguration.Code == -1)
                {
                    SetMovieItem();
                }
            }

            pnlCF.SuspendLayout();
            pnlCF.Controls.Add(tvCodeFinder);
            pnlCF.ResumeLayout();

            panel1.SuspendLayout();
            panel1.Controls.Add(movieCodeFinder);
            panel1.ResumeLayout();

            UpdateDirectoryDropDown(cbDirectory, TVSettings.Instance.LibraryFolders, TVSettings.Instance.DefShowLocation, TVSettings.Instance.DefShowAutoFolders && TVSettings.Instance.DefShowUseDefLocation, tpTV);
            UpdateDirectoryDropDown(cbMovieDirectory, TVSettings.Instance.MovieLibraryFolders, TVSettings.Instance.DefMovieDefaultLocation, true, tpMovie);
        }
Exemplo n.º 12
0
 private void FileIsMissing(MovieConfiguration si, string folder, bool missCheck)
 {
     // second part of missing check is to see what is missing!
     if (missCheck)
     {
         // then add it as officially missing
         Doc.TheActionList.Add(new MovieItemMissing(si, folder));
     } // if doing missing check
 }
Exemplo n.º 13
0
        public static void SearchForMovie(MovieConfiguration mov)
        {
            string       serverName = TVSettings.Instance.JackettServer;
            string       serverPort = TVSettings.Instance.JackettPort;
            const string FORMAT     = "{ShowName} ({Year})";

            string url = $"http://{serverName}:{serverPort}/UI/Dashboard#search={WebUtility.UrlEncode(CustomMovieName.NameFor(mov, FORMAT))}&tracker=&category=";

            Helpers.OpenUrl(url);
        }
Exemplo n.º 14
0
        private static string NormalJackettUrl(MovieConfiguration actionMovieConfig)
        {
            string serverName = TVSettings.Instance.JackettServer;
            string serverPort = TVSettings.Instance.JackettPort;
            string allIndexer = TVSettings.Instance.JackettIndexer;
            string apikey     = TVSettings.Instance.JackettAPIKey;

            return
                ($"http://{serverName}:{serverPort}{allIndexer}/api?t=movie&apikey={apikey}&tmdbid={actionMovieConfig.TmdbCode}");
        }
Exemplo n.º 15
0
 public ActionCopyMoveRename(Op operation, FileInfo from, FileInfo to, MovieConfiguration mc, bool doTidyup, ItemMissing?undoItem, TVDoc tvDoc)
 {
     Tidyup          = doTidyup ? TVSettings.Instance.Tidyup : null;
     PercentDone     = 0;
     Movie           = mc;
     Operation       = operation;
     From            = from;
     To              = to;
     UndoItemMissing = undoItem;
     doc             = tvDoc;
 }
Exemplo n.º 16
0
        private static string TextJackettUrl(MovieConfiguration actionMovieConfig)
        {
            string       serverName = TVSettings.Instance.JackettServer;
            string       serverPort = TVSettings.Instance.JackettPort;
            string       allIndexer = TVSettings.Instance.JackettIndexer;
            string       apikey     = TVSettings.Instance.JackettAPIKey;
            const string FORMAT     = "{ShowName}";
            string?      text       = WebUtility.UrlEncode(CustomMovieName.NameFor(actionMovieConfig, FORMAT));

            return
                ($"http://{serverName}:{serverPort}{allIndexer}/api?t=movie&q={text}&apikey={apikey}");
        }
Exemplo n.º 17
0
        public override ItemList?ProcessMovie(MovieConfiguration movie, FileInfo file, bool forceRefresh)
        {
            //If we have KODI New style images being downloaded then we want to check that 3 files exist
            //for the cachedSeries:
            //http://wiki.xbmc.org/index.php?title=XBMC_v12_(Frodo)_FAQ#Local_images
            //poster
            //banner
            //fanart

            if (TVSettings.Instance.KODIImages)
            {
                ItemList theActionList = new ItemList();
                string   baseFileName  = file.MovieFileNameBase();

                FileInfo posterJpg = FileHelper.FileInFolder(file.Directory, baseFileName + "-poster.jpg");
                FileInfo bannerJpg = FileHelper.FileInFolder(file.Directory, baseFileName + "-banner.jpg");
                FileInfo fanartJpg = FileHelper.FileInFolder(file.Directory, baseFileName + "-fanart.jpg");

                if ((forceRefresh || !posterJpg.Exists) && !donePosterJpg.Contains(file.Directory.FullName))
                {
                    string path = movie.CachedMovie?.PosterUrl;
                    if (!string.IsNullOrEmpty(path))
                    {
                        theActionList.Add(new ActionDownloadImage(movie, null, posterJpg, path, false));
                        donePosterJpg.Add(file.Directory.FullName);
                    }
                }

                if ((forceRefresh || !bannerJpg.Exists) && !doneBannerJpg.Contains(file.Directory.FullName))
                {
                    string path = string.Empty; //todo link up movir banner url movie.CachedMovie?.BannerUrl;
                    if (!string.IsNullOrEmpty(path))
                    {
                        theActionList.Add(new ActionDownloadImage(movie, null, bannerJpg, path, false));
                        doneBannerJpg.Add(file.Directory.FullName);
                    }
                }

                if ((forceRefresh || !fanartJpg.Exists) && !doneFanartJpg.Contains(file.Directory.FullName))
                {
                    string path = movie.CachedMovie?.FanartUrl;
                    if (!string.IsNullOrEmpty(path))
                    {
                        theActionList.Add(new ActionDownloadImage(movie, null, fanartJpg, path));
                        doneFanartJpg.Add(file.Directory.FullName);
                    }
                }
                return(theActionList);
            }

            return(base.ProcessMovie(movie, file, forceRefresh));
        }
        private bool UpdateDirectory(MovieConfiguration si, string folder)
        {
            DirectoryInfo di      = new DirectoryInfo(folder);
            bool          goAgain = !di.Exists;

            if (di.Exists)
            {
                si.ManualLocations.Add(folder);
                Doc.SetDirty();
            }

            return(goAgain);
        }
Exemplo n.º 19
0
        private void AddToLibrary([NotNull] PossibleNewMovie ai)
        {
            if (ai.CodeUnknown)
            {
                return;
            }

            string?matchingRoot = TVSettings.Instance.MovieLibraryFolders.FirstOrDefault(s => ai.Directory.FullName.IsSubfolderOf(s));
            bool   isInLibraryFolderFileFinder = matchingRoot.HasValue();

            // see if there is a matching show item
            MovieConfiguration found = mDoc.FilmLibrary.GetMovie(ai);

            if (found is null)
            {
                AddNewMovieToLibrary(ai, isInLibraryFolderFileFinder, matchingRoot);
                return;
            }

            //We are updating an existing record

            string targetDirectoryName = CustomMovieName.NameFor(found, TVSettings.Instance.MovieFolderFormat);
            bool   inDefaultPath       = ai.Directory.Name.Equals(
                targetDirectoryName,
                StringComparison.CurrentCultureIgnoreCase);

            bool existingLocationIsDefaultToo = found.UseAutomaticFolders && found.AutomaticFolderRoot.In(TVSettings.Instance.MovieLibraryFolders.ToArray());

            if (inDefaultPath && isInLibraryFolderFileFinder && !existingLocationIsDefaultToo)
            {
                found.UseAutomaticFolders       = true;
                found.UseCustomFolderNameFormat = false;
                found.AutomaticFolderRoot       = matchingRoot !;
                //leave found.UseManualLocations alone to retain any existing manual locations
                return;
            }

            //we have an existing record that we need to add manual folders to

            if (isInLibraryFolderFileFinder && !found.AutomaticFolderRoot.HasValue())
            {
                //Probably in the library
                found.AutomaticFolderRoot = matchingRoot !;
            }

            found.UseManualLocations = true;
            if (!found.ManualLocations.Contains(ai.Directory.FullName))
            {
                found.ManualLocations.Add(ai.Directory.FullName);
            }
        }
Exemplo n.º 20
0
        protected override void Do()
        {
            using (System.IO.StreamWriter file = new System.IO.StreamWriter(Location()))
            {
                file.WriteLine("Movie Name,Year,Folder,Nice Name");

                foreach (MovieItemMissing im in TheActionList.MissingMovies.ToList())
                {
                    MovieConfiguration pe = im.MovieConfig;
                    file.WriteLine(
                        $"\"{pe.ShowName}\",{pe.CachedMovie?.Year},\"{im.TargetFolder}\",\"{im.Filename}\"");
                }
            }
        }
Exemplo n.º 21
0
 private void SetMovieFolderType([NotNull] MovieConfiguration si)
 {
     if (si.UseAutomaticFolders)
     {
         if (si.UseCustomFolderNameFormat)
         {
             rdoFolderCustom.Checked = true;
         }
         else
         {
             rdoFolderLibraryDefault.Checked = true;
         }
     }
 }
Exemplo n.º 22
0
        private void SetupLanguages([NotNull] MovieConfiguration si)
        {
            chkCustomLanguage.Checked = si.UseCustomLanguage;
            if (chkCustomLanguage.Checked)
            {
                Language languageFromCode =
                    TheTVDB.LocalCache.Instance.LanguageList?.GetLanguageFromCode(si.CustomLanguageCode);

                if (languageFromCode != null)
                {
                    cbLanguage.Text = languageFromCode.Name;
                }
            }

            cbLanguage.Enabled = chkCustomLanguage.Checked;
        }
Exemplo n.º 23
0
        private static bool RejectFolderIfIncludedInShow(bool fullLogging, [NotNull] MovieConfiguration si, string theFolder)
        {
            foreach (string dir in si.Locations)
            {
                if (!string.IsNullOrEmpty(dir) && theFolder.IsSubfolderOf(dir))
                {
                    // we're looking at a folder that is a subfolder of an existing show
                    if (fullLogging)
                    {
                        Logger.Info($"Rejecting {theFolder} as it's already part of {si.ShowName}.");
                    }

                    return(true);
                }
            }

            return(false);
        }
Exemplo n.º 24
0
        public static string NameFor(MovieConfiguration m, string styleString, string?extension)
        {
            string r = NameFor(m, styleString);

            if (string.IsNullOrEmpty(extension))
            {
                return(r);
            }

            bool needsSpacer = !extension.StartsWith(".", StringComparison.Ordinal);

            if (needsSpacer)
            {
                return(r + "." + extension);
            }

            return(r + extension);
        }
Exemplo n.º 25
0
        public override ItemList?ProcessMovie(MovieConfiguration movie, FileInfo file, bool forceRefresh)
        {
            DateTime?updateTime = movie.CachedMovie?.FirstAired;

            if (!TVSettings.Instance.CorrectFileDates || !updateTime.HasValue)
            {
                return(base.ProcessMovie(movie, file, forceRefresh));
            }

            DateTime newUpdateTime = Helpers.GetMinWindowsTime(updateTime.Value);

            DirectoryInfo di          = file.Directory;
            ItemList      returnItems = new ItemList();

            try
            {
                if (di.LastWriteTimeUtc != newUpdateTime && !doneFilesAndFolders.Contains(di.FullName))
                {
                    doneFilesAndFolders.Add(di.FullName);
                    returnItems.Add(new ActionDateTouchMedia(di, movie, newUpdateTime));
                }
            }
            catch (Exception)
            {
                doneFilesAndFolders.Add(di.FullName);
                returnItems.Add(new ActionDateTouchMedia(di, movie, newUpdateTime));
            }

            try
            {
                if (file.LastWriteTimeUtc != newUpdateTime && !doneFilesAndFolders.Contains(file.FullName))
                {
                    doneFilesAndFolders.Add(file.FullName);
                    returnItems.Add(new ActionDateTouchMovie(file, movie, newUpdateTime));
                }
            }
            catch (Exception)
            {
                doneFilesAndFolders.Add(file.FullName);
                returnItems.Add(new ActionDateTouchMovie(file, movie, newUpdateTime));
            }

            return(returnItems);
        }
Exemplo n.º 26
0
        private void SetupDropDowns([NotNull] MovieConfiguration si)
        {
            if (TheTVDB.LocalCache.Instance.LanguageList != null) //This means that language shave been loaded
            {
                string pref = string.Empty;
                cbLanguage.BeginUpdate();
                cbLanguage.Items.Clear();
                foreach (Language l in TheTVDB.LocalCache.Instance.LanguageList)
                {
                    cbLanguage.Items.Add(l.Name);

                    if (si.CustomLanguageCode == l.Abbreviation)
                    {
                        pref = l.Name;
                    }
                }
                cbLanguage.EndUpdate();
                cbLanguage.Text = pref;
            }
        }
Exemplo n.º 27
0
        protected override void Check(MovieConfiguration si, DirFilesCache dfc, TVDoc.ScanSettings settings)
        {
            List <string> allFolders = si.Locations.ToList();

            if (allFolders.Count == 0) // no folders defined for this show
            {
                return;                // so, nothing to do.
            }

            // process each folder for show...
            foreach (string folder in allFolders)
            {
                if (settings.Token.IsCancellationRequested)
                {
                    return;
                }

                CheckMovieFolder(si, dfc, settings, folder);
            }
        }
Exemplo n.º 28
0
        public AutoAddShow(string hint, string filename)
        {
            InitializeComponent();
            ShowConfiguration  = new ShowConfiguration();
            MovieConfiguration = new MovieConfiguration();
            bool assumeMovie = FinderHelper.IgnoreHint(hint);

            lblFileName.Text = "Filename: " + filename;

            tvCodeFinder = new TheTvdbCodeFinder("")
            {
                Dock = DockStyle.Fill
            };
            movieCodeFinder = new TmdbCodeFinder("")
            {
                Dock = DockStyle.Fill
            };

            (!assumeMovie ? tpTV : tpMovie).Show();

            tvCodeFinder.SetHint(hint);
            movieCodeFinder.SetHint(hint);

            tvCodeFinder.SelectionChanged    += MTCCF_SelectionChanged;
            movieCodeFinder.SelectionChanged += MTCCF_SelectionChanged;

            pnlCF.SuspendLayout();
            pnlCF.Controls.Add(tvCodeFinder);
            pnlCF.ResumeLayout();

            panel1.SuspendLayout();
            panel1.Controls.Add(movieCodeFinder);
            panel1.ResumeLayout();

            ActiveControl = (!assumeMovie ? tvCodeFinder : movieCodeFinder); // set initial focus to the code entry/show finder control

            UpdateDirectoryDropDown(cbDirectory, TVSettings.Instance.LibraryFolders, TVSettings.Instance.DefShowLocation, TVSettings.Instance.DefShowAutoFolders && TVSettings.Instance.DefShowUseDefLocation, tpTV);
            UpdateDirectoryDropDown(cbMovieDirectory, TVSettings.Instance.MovieLibraryFolders, TVSettings.Instance.DefMovieDefaultLocation, true, tpMovie);

            originalHint = hint;
        }
Exemplo n.º 29
0
        private void SetProvider([NotNull] MovieConfiguration si)
        {
            switch (si.ConfigurationProvider)
            {
            case TVDoc.ProviderType.libraryDefault:
            case TVDoc.ProviderType.TVmaze:
                rdoDefault.Checked = true;
                break;

            case TVDoc.ProviderType.TheTVDB:
                rdoTVDB.Checked = true;
                break;

            case TVDoc.ProviderType.TMDB:
                rdoTMDB.Checked = true;
                break;

            default:
                throw new ArgumentOutOfRangeException();
            }
        }
Exemplo n.º 30
0
        private static string CreateHtml([NotNull] MovieConfiguration si)
        {
            CachedMovieInfo cachedSeries = si.CachedMovie;

            if (cachedSeries is null)
            {
                return(string.Empty);
            }

            string yearRange  = cachedSeries.Year?.ToString() ?? "";
            string stars      = ShowHtmlHelper.StarRating(cachedSeries.SiteRating / 2);
            string genreIcons = string.Join("&nbsp;", cachedSeries.Genres.Select(ShowHtmlHelper.GenreIconHtml));
            string siteRating = cachedSeries.SiteRating > 0 ? cachedSeries.SiteRating + "/10" : "";

            string poster      = ShowHtmlHelper.CreatePosterHtml(cachedSeries);
            string runTimeHtml = string.IsNullOrWhiteSpace(cachedSeries.Runtime) ? string.Empty : $"<br/> {cachedSeries.Runtime} min";

            return($@"<div class=""card card-body"">
            <div class=""row"">
            <div class=""col-md-4"">
                {poster}
</div>
            <div class=""col-md-8 d-flex flex-column"">
                <div class=""row"">
                    <div class=""col-md-8""><h1>{si.ShowName}</h1><small class=""text-muted"">{cachedSeries.TagLine}</small></div>
                    <div class=""col-md-4 text-right""><h6>{yearRange} ({cachedSeries.Status})</h6>
<small class=""text-muted"">{cachedSeries.ShowLanguage} - {cachedSeries.Type}</small>
                        <small class=""text-muted"">{runTimeHtml}</small></div>
</div>
                
            <div><blockquote>{cachedSeries.Overview}</blockquote></div>
            <div><blockquote>{cachedSeries.GetActorNames().ToCsv()}</blockquote></div>
            <div class=""row align-items-bottom flex-grow-1"">
                <div class=""col-md-4 align-self-end"">{stars}<br>{siteRating}{ShowHtmlHelper.AddRatingCount(cachedSeries.SiteRatingVotes)}</div>
                <div class=""col-md-4 align-self-end text-center"">{cachedSeries.ContentRating}<br>{cachedSeries.Network}</div>
                <div class=""col-md-4 align-self-end text-right"">{genreIcons}<br>{cachedSeries.Genres.ToCsv()}</div>
            </div>
            </div></div></div>");
        }