diff --git a/source/net/filebot/format/MediaBindingBean.java b/source/net/filebot/format/MediaBindingBean.java index f868ff99..3e4d64a7 100644 --- a/source/net/filebot/format/MediaBindingBean.java +++ b/source/net/filebot/format/MediaBindingBean.java @@ -55,7 +55,7 @@ import net.filebot.media.FFProbe; import net.filebot.media.ImageMetadata; import net.filebot.media.LocalDatasource.PhotoFile; import net.filebot.media.MetaAttributes; -import net.filebot.media.PlexNamingStandard; +import net.filebot.media.NamingStandard; import net.filebot.media.VideoFormat; import net.filebot.mediainfo.MediaInfo; import net.filebot.mediainfo.MediaInfo.StreamKind; @@ -201,7 +201,7 @@ public class MediaBindingBean { } // enforce title length limit by default - return truncateText(t, PlexNamingStandard.TITLE_MAX_LENGTH); + return truncateText(t, NamingStandard.TITLE_MAX_LENGTH); } @Define("d") @@ -1074,7 +1074,18 @@ public class MediaBindingBean { @Define("plex") public File getPlexStandardPath() throws Exception { - String path = new PlexNamingStandard().getPath(infoObject); + String path = NamingStandard.PLEX.getPath(infoObject); + try { + path = path.concat(getSubtitleTags()); // NPE if {subt} is undefined + } catch (Exception e) { + // ignore => no language tags + } + return new File(path); + } + + @Define("kodi") + public File getKodiStandardPath() throws Exception { + String path = NamingStandard.KODI.getPath(infoObject); try { path = path.concat(getSubtitleTags()); // NPE if {subt} is undefined } catch (Exception e) { diff --git a/source/net/filebot/media/PlexNamingStandard.java b/source/net/filebot/media/NamingStandard.java similarity index 82% rename from source/net/filebot/media/PlexNamingStandard.java rename to source/net/filebot/media/NamingStandard.java index a39f2051..4ce495b4 100644 --- a/source/net/filebot/media/PlexNamingStandard.java +++ b/source/net/filebot/media/NamingStandard.java @@ -13,8 +13,32 @@ import net.filebot.web.Episode; import net.filebot.web.EpisodeFormat; import net.filebot.web.Movie; import net.filebot.web.MoviePart; +import net.filebot.web.MultiEpisode; -public class PlexNamingStandard { +public class NamingStandard { + + /** + * Plex Naming Standard (i.e. Default Implementation) + * + * https://support.plex.tv/articles/categories/media-preparation/ + */ + public static final NamingStandard PLEX = new NamingStandard(); + + /** + * Kodi Naming Standard (i.e. same as Plex, but with SxE and non-range based multi-episode formatting) + * + * https://kodi.wiki/view/Naming_video_files + */ + public static final NamingStandard KODI = new NamingStandard() { + + @Override + public String formatEpisodeNumbers(Episode e) { + if (e instanceof MultiEpisode) { + return EpisodeFormat.SeasonEpisode.formatMultiSxE((MultiEpisode) e); + } + return EpisodeFormat.SeasonEpisode.formatSxE(e); + } + }; public String getPath(Object o) { if (o instanceof Episode) @@ -133,7 +157,6 @@ public class PlexNamingStandard { // TV Series return EpisodeFormat.SeasonEpisode.formatS00E00(e); } - } public String formatEpisodeTitle(Episode e) { diff --git a/source/net/filebot/ui/rename/BindingDialog.properties b/source/net/filebot/ui/rename/BindingDialog.properties index 871f498c..085b49a3 100644 --- a/source/net/filebot/ui/rename/BindingDialog.properties +++ b/source/net/filebot/ui/rename/BindingDialog.properties @@ -2,4 +2,4 @@ parameter.exclude: ^StreamKind|^UniqueID|^StreamOrder|^ID|Count$ # preview expressions (keys are tagged so they can be sorted alphabetically) -expressions: n, y, s, e, sxe, s00e00, t, d, startdate, absolute, ny, es, sy, sc, di, dc, age, special, episode, series, primaryTitle, alias, movie, tmdbid, imdbid, pi, pn, lang, subt, plex, az, type, anime, regular, music, album, artist, albumArtist, actors, director, collection, genre, genres, languages, runtime, certification, rating, votes, vc, ac, cf, vf, hpi, aco, af, channels, resolution, dim, width, height, bitdepth, hdr, bitrate, kbps, mbps, khz, ws, hd, source, tags, s3d, group, original, info, info.network, info.status, info.productionCompanies, info.productionCountries, info.certifications, info.certifications.AU, info.certifications.DE, omdb.rating, omdb.votes, localize.deu.n, localize.deu.t, localize.zho.n, localize.zho.t, order.airdate.sxe, order.dvd.sxe, fn, ext, mediaType, mediaPath, file, file.name, folder, folder.name, mediaTitle, audioLanguages, textLanguages, duration, seconds, minutes, hours, bytes, megabytes, gigabytes, crc32, media.title, media.collection, media.season, media.part, media.partID, media.genre, media.contentType, media.description, media.lyrics, video[0].codecID, video[0].frameRate, video[0].displayAspectRatioString, video[0].scanType, audio.language, audio[0].bitRateString, audio[0].language, text.language, text[0].language, text[0].codecInfo, camera, camera.maker, camera.model, location, location.country +expressions: n, y, s, e, sxe, s00e00, t, d, startdate, absolute, ny, es, sy, sc, di, dc, age, special, episode, series, primaryTitle, alias, movie, tmdbid, imdbid, pi, pn, lang, subt, plex, plex.name, kodi, kodi.name, az, type, anime, regular, music, album, artist, albumArtist, actors, director, collection, genre, genres, languages, runtime, certification, rating, votes, vc, ac, cf, vf, hpi, aco, af, channels, resolution, dim, width, height, bitdepth, hdr, bitrate, kbps, mbps, khz, ws, hd, source, tags, s3d, group, original, info, info.network, info.status, info.productionCompanies, info.productionCountries, info.certifications, info.certifications.AU, info.certifications.DE, omdb.rating, omdb.votes, localize.deu.n, localize.deu.t, localize.zho.n, localize.zho.t, order.airdate.sxe, order.dvd.sxe, fn, ext, mediaType, mediaPath, file, file.name, folder, folder.name, mediaTitle, audioLanguages, textLanguages, duration, seconds, minutes, hours, bytes, megabytes, gigabytes, crc32, media.title, media.collection, media.season, media.part, media.partID, media.genre, media.contentType, media.description, media.lyrics, video[0].codecID, video[0].frameRate, video[0].displayAspectRatioString, video[0].scanType, audio.language, audio[0].bitRateString, audio[0].language, text.language, text[0].language, text[0].codecInfo, camera, camera.maker, camera.model, location, location.country diff --git a/source/net/filebot/web/EpisodeFormat.java b/source/net/filebot/web/EpisodeFormat.java index d615f3f6..624a9540 100644 --- a/source/net/filebot/web/EpisodeFormat.java +++ b/source/net/filebot/web/EpisodeFormat.java @@ -119,6 +119,21 @@ public class EpisodeFormat extends Format { }).collect(joining(" - ")); } + public String formatMultiSxE(Iterable episodes) { + return formatMultiNumbers(episodes, "%01dx", "%02d", "x"); + } + + public String formatMultiS00E00(Iterable episodes) { + return formatMultiNumbers(episodes, "S%02d", "E%02d", "-"); + } + + public String formatMultiNumbers(Iterable episodes, String seasonFormat, String episodeFormat, String delimiter) { + return getSeasonEpisodeNumbers(episodes).entrySet().stream().map(it -> { + String s = it.getKey() >= 0 ? String.format(seasonFormat, it.getKey()) : ""; + return it.getValue().stream().distinct().map(i -> String.format(episodeFormat, i)).collect(joining(delimiter, s, "")); + }).collect(joining(" - ")); + } + private SortedMap> getSeasonEpisodeNumbers(Iterable episodes) { SortedMap> n = new TreeMap>(); for (Episode it : episodes) { diff --git a/source/net/filebot/web/MultiEpisode.java b/source/net/filebot/web/MultiEpisode.java index 6e105c2b..bf95d0d3 100644 --- a/source/net/filebot/web/MultiEpisode.java +++ b/source/net/filebot/web/MultiEpisode.java @@ -5,9 +5,10 @@ import static java.util.Collections.*; import static java.util.stream.Collectors.*; import java.util.Arrays; +import java.util.Iterator; import java.util.List; -public class MultiEpisode extends Episode { +public class MultiEpisode extends Episode implements Iterable { protected Episode[] episodes; @@ -23,6 +24,11 @@ public class MultiEpisode extends Episode { this.episodes = episodes.toArray(new Episode[0]); } + @Override + public Iterator iterator() { + return stream(episodes).iterator(); + } + public List getEpisodes() { return unmodifiableList(asList(episodes)); }