Jellyfin compatible naming scheme

Any questions? Need some help?
Post Reply
smellycheese
Posts: 12
Joined: 27 Sep 2025, 23:12

Jellyfin compatible naming scheme

Post by smellycheese »

Hello,

Following a conversation with Rednoah, here is a topic about renaming movies to use them on Jellyfin/Emby and taking in consider multiple patterns like tags, quality, audio type (the different variants of French in particular) and few other concern.

As invited by our host, I'll slice this goal in multiple sub thread by all will link to this one for complete scenario and final result (I guess !)

My expectation is to use filebot with the following constrain and rules :

Current filename may include original title or localized title.

I'd like to group the different files corresponding to the same film into a single directory named with :

original_title.year.tmdbid-tag_imdb

Then files in directory would be named like this :

original.title.yar.imdbid-tag_tmdb - "normalized_resolution" "particularities, if existing in original filename" "MULTi, if multiple audio track exist" "quality, if existing in original filename" "audio_type".ext

With :

original title : {primaryTitle}
year : {y}
tmdbid : {"{tmdb-$tmdbid}"}

- normalized_resolution : 2160p, 1080p, 1080i, 720p, 480p, etc, from file metadata. I will use {vf}

- particularities (if multiples exist, sort them alphabetically if possible) : 3D, REMASTERED, EXTENDED, THEATRiCAL, DiRECTORS.CUT, UNRATED, UNCUT, PROPER, iNTERNAL, LiMiTED, REPACK, PROPER, CuSTOM, Open.Matte, COMPLETE, SPECIAL.EDITION, RATED, CRiTERiON, Cannes.Cut, FESTiVAL, XXth.Anniversary.Edition ( where XX could be any number), ULTiMATE.DiRECTORS.CUT, 3D, HDR, DV
particularities must respect type case as described above, if that's not currently true in original filename it must be fixed in new filename.
{tags} could be an idea, but some of these pattern aren't included if I'm right.

- MULTi, if exist in original filename or if multiple language audio tracks are detected. I could have multiple audio tracks of the same language (ie audio description, comment, different codec) that don't match for MULTi. But, by example for an English movie, if I've an French audio track, I only will have comment audio track in English if I've also an English audio track for the movie itself.

- quality : BluRay, WEB, REMUX, 4KLight, HDLight, WEBRip, VHSRip, DVDRip, HDTV, TVRip, BDRip
same thing for quality, type case must be followed or fixed.

- audio type :
VFF, VOF, VOQ, VFQ, VFI, VF2, VOST, VOSTFR, MUET : if this abbreviation exist in original filename, it must be added to new filename.
If FRENCH exist in original filename :
Check if the country of origin of the movie is France, if so add VOF to new filename
if not, check if the country of origin of the movie is Canada, if so add VOQ to new filename
if not, left FRENCH in new filename
If nothing match with above rules, check audio language on file metadata.
If FRENCH exist then ADD FRENCH to new filename
If multiple language exist ADD MULTi to new filename
If an audio track language match with country of origin of the movie, with FRENCH subtitle, then add VOSTFR to new filename
If an audio track language match with country of origin of the movie, without FRENCH subtitle, then add VOST to new filename
If an audio track language match with country of origin of the movie, without any subtitle, then add VO to new filename
If no audio track is found or couldn't be identified, then add UNKAUD to new filename

Here is two examples :

directory "Highlander.1986.tmdbid-8009" contain the following files (without '') :

Format: Select all

'Highlander.1986.tmdbid-8009 - 1080p DiRECTOR CUT MULTi VFF.mkv'	    was	Highlander.1986.DiRECTORS.CUT.1080p.BluRay.x264.DTS.VFF.mkv
'Highlander.1986.tmdbid-8009 - 1080p REMASTERED VO.mkv' 		        was	Highlander.1986.REMASTERED.1080p.BluRay.x265.AAC.VO.mp4
'Highlander.1986.tmdbid-8009 - 2160p BluRay MULTi REMUX VFF.mkv'	    was	Highlander.1986.MULTi.2160p.BluRay.REMUX.HEVC.DTS.VFF.mkv
'Highlander.1986.tmdbid-8009 - 2160p 4KLight MULTi VFF.mkv'	            was	Highlander.1986.MULTi.2160p.10bit.4KLight.BluRay.HDR.x265.AC3.VFF.mkv
'Highlander.1986.tmdbid-8009 - 360p VFF.avi'			                was	Highlander.1986.360p.DivX.MP3.VFF.avi

directory "The.Martian.2015.tmdbid-286217" contain the following files (without '') :

Format: Select all

'The.Martian.2015.tmdbid-286217 - 1080p BluRay MULTi VFF.mkv'			        was The.Martian.2015.MULTi.1080p.BluRay.x264.DTS.VFF.mkv
'The.Martian.2015.tmdbid-286217 - 1080p BDRip CuSTOM MULTi VFQ.mkv'			    was	The.Martian.2015.CuSTOM.MULTi.720p.BDRip.x264.AC3.VFQ.mkv
'The.Martian.2015.tmdbid-286217 - 1080p 3D BluRay COMPLETE MULTi VFF.iso'		was	The.Martian.2015.3D.MULTi.1080p.COMPLETE.BluRay.VFF.iso
'The.Martian.2015.tmdbid-286217 - 2160p 4KLight HDR MULTi THEATRiCAL VFF.mkv'	was	The.Martian.2015.THEATRiCAL.MULTi.2160p.4KLight.HDR.AC3.VFF.mkv
'The.Martian.2015.tmdbid-286217 - 2160p EXTENDED HDR MULTi WEB-DL VFQ.mkv'	    was	The.Martian.2015.EXTENDED.2160p.MA.WEB-DL.TrueHD.Atmos.7.1.HDR.H.265-FLUX.mkv
With these 2 examples, I tried to cover different things.

If movie title in original filename isn't the original title, change it in new filename for original title.
Non Ascii characters must be transliterated.

For each file, I also want an nfo file, in destination folder, named as the movie file but with .nfo extension.
This nfo must be compatible with Emby/Jellyfin and include the following informations :

original title
year
imdb tag
tmdb tag
sha256 (or any other relevant hash to retrieve file in case of filename mismatch between movie file and nfo file)
file size in bytes
original filename

If a file can't be identified, left it untouched and log an error (default behavior of filebot if I'm right).
If multiples files lead to the same new filename, then proceed with the first and left other where is it and add .double at the end of the filename.

I'm aware that hash will significantly extend processing time.
I considered post-processing this by adding the hash via another script, but while the initial processing will take a long time, the additions should be very quick.

My goal is to group files from differents quality and keep only relevant files.
Name scheme and nfos is expected to allow a better experience/recognition from Emby/Jellyfin

I'm open to any suggestion.
smellycheese
Posts: 12
Joined: 27 Sep 2025, 23:12

Re: Jellyfin compatible naming scheme

Post by smellycheese »

Hello,

Thank to the tremendous and incredibly patient Rednoah and after multiple change, I finally get a satisfactory result.

Here is the current complete code :

Format: Select all

{
    def romanLanguages = ['en','fr','de','es','it','pt','nl','sv','da','no','fi','pl','cs','hu','ro','tr','el','la'] // list all Countries using Roman alphabel
    def romanLang = any{primaryTitle.language}{'en'} in romanLanguages // Check if movie title meet romanLanguage, if not we use for English title (more easier to read than transliterated title)
    def conjunction = (any{country}{'US'} == 'FR') ? 'et' : 'and'  // 'US' as default value

    (romanLang ? primaryTitle : primaryTitle.english) // if title isn't matching romanLangugage we use English title (more easier to read than transliterated title)
        .ascii()
        .replaceAll(/&/, conjunction)  // Replace & by "et" or "and"
        .replaceAll(/[^a-zA-Z0-9\s]+/, '')  // Remove all special characters except spaces
        .replaceAll(/\s+/, ' ')  // Normalizes multiple spaces into one
        .trim()  // Removes leading/trailing spaces
}
({y})
[{"tmdbid-$tmdbid"}] /
{
    def romanLanguages = ['en','fr','de','es','it','pt','nl','sv','da','no','fi','pl','cs','hu','ro','tr','el','la'] // list all Countries using Roman alphabel
    def romanLang = any{primaryTitle.language}{'en'} in romanLanguages// Check if movie title meet romanLanguage,
    def conjunction = (any{country}{'US'} == 'FR') ? 'et' : 'and'  // 'US' as default value

    (romanLang ? primaryTitle : primaryTitle.english) // if title isn't matching romanLangugage we use English title (more easier to read than transliterated title)
        .ascii()
        .replaceAll(/&/, conjunction)  // Replace & by "et" or "and"
        .replaceAll(/[^a-zA-Z0-9\s]+/, '')  // Remove all special characters except spaces
        .replaceAll(/\s+/, ' ')  // Normalizes multiple spaces into one
        .trim()  // Removes leading/trailing spaces
}
({y})
[{"tmdbid-$tmdbid"}] -
{vf}
{
        fn.match(/([0-9]{1,2})th.Anniversary.Edition/) + ' Anniversary Edition'
}
{
        def pattern = /3D|REMASTERED|EXTENDED|THEATRiCAL|DiRECTORS.CUT|UNRATED|UNCUT|PROPER|iNTERNAL|LiMiTED|REPACK|PROPER|CuSTOM|Open.Matte|COMPLETE|SPECIAL.EDITION|RATED|CRiTERiON|Cannes.Cut|FESTiVAL|ULTiMATE.DiRECTORS.CUT|3D/
        def mapping = pattern.tokenize('|').collectEntries{ [it, it] }
        fn.matchAll(pattern)*.match(mapping).sort().unique().join(' ')
}
{ fn.matchAll(/BluRay|WEB|WEB-DL|WEBRip|VHSRip|DVDRip|HDTV|TVRip|BDRip/).join(' ') }
{ fn.matchAll(/REMUX|4KLight|HDLight/).join(' ') }
{ video.HDR_Format =~ /Dolby Vision/ ? video.HDR_Format_Compatibility =~ /HDR10/ ? 'DV HDR10' : 'DV' : HDR }
{vc}
{ac}
{audioLanguages.size() > 1 ? 'MULTi' : null}
{
        any {
            return fn.match(/VFF|VOF|VOQ|VFQ|VFI|VF2|VOST|VOSTFR|MUET/)
        }{
            // If file name contains FRENCH or file has a French audio stream
            if (fn =~ /FRENCH/ || audioLanguages =~ /fra/) {
                    return country.match('FR':'VOF', 'CA':'VOQ') ?: 'FRENCH'
            }
        }{
            // If an audio track language match with original language of the movie
            if (audioLanguages =~ language) {
                    // with FRENCH subtitle, then add VOSTFR to new filename
                    // without FRENCH subtitle, then add VOST to new filename
                    // without any subtitle, then add VO to new filename
                    return any{ textLanguages =~ /fra/ ? 'VOSTFR' : 'VOST' }{ 'VO' }
            }
        }
}
{
    // Team extract either from filename or metadata
    def team =  any{ fn.match(/^\[(.*?)\]/) }
                { fn.match(/\[([^\[\]]+)\]+/) }
                { fn.match(/[-]([^-]+)$/) }
                { group }

    // add Team preceded by a dash
    team ? '-' + team : ''
}
which give

Title (Year) [tmdbid-ID] / Title (Year) [tmdbid-ID] - Resolution TAGS Source Vcodec Acodec MULTi? Audiotype-Group

Some change happen from the first request.

First, Jellyfin encourages using titles as close to the original as possible. (https://jellyfin.org/docs/general/server/media/movies)

In my case, I add the tmdbid attribute, so in principle, even if the title doesn't allow users to find the movie, the tmdbid attribute should solve the problem.
I was able to test and confirm that using periods instead of spaces also works.

However, the various threads I consulted on this topic strongly discourage the use of periods in the title.

There have also been problems in the past (example: https://github.com/jellyfin/jellyfin/issues/5224).

Considering that in my case I will have multiple versions of some movies, and that in this case the separator "-" is mandatory between the base of the filename (=directory name) and the rest (tag, resolution, etc.), I would have spaces anyway.

So, let's stick as closely as possible to the recommendations, hence the return to the title with spaces...

I've also considered {jellyfin} for a time but regarding other change I apply to the title, I prefer to stick to this work.

I hadn't mentioned the {group} case; I was planning to include it in an NFO file, but the post-processing method doesn't suit me, so I'll proceed differently.
Following trash guides, {group} is an information to retain, so I'm setting aside the NFO file (which I might handle elsewhere) and keeping the information into the filename.

But there's a catch with this part.

The code for {group}, shamelessly taken from here viewtopic.php?t=4, can lead to detection errors, for example, if the source filename is DTS-HDMA or WEB-DL, or if the group name itself contains a hyphen.

So, there are a few preparation steps that might be integrated into this code, but I haven't looked into it yet. For now, I prefer to prepare the files in advance; too many special cases to handle would end up creating a nightmare of code i think.

I moved HDR/DV outside of TAGS because detecting from metadata is more suitable than reporting existing TAG that could be incomplete or wrong.
I also have considered DUAL on side of MULTi, but that's more likely a corner case (for me) which have complicated much more the code I think.

I've added annotations based on my understanding. If any points don't match the code in question, please feel free to gently point them out to me :)

I remain open to comments and suggestions to improve this code, I have only recently discovered all of this.

Hoping this will be helpful to others.
User avatar
rednoah
The Source
Posts: 24412
Joined: 16 Nov 2011, 08:59
Location: Taipei
Contact:

Re: Jellyfin compatible naming scheme

Post by rednoah »

:!: I note that primaryTitle is a String object, and thus does not have a language nor a english property, so this always fails with Binding "language": No such property. As a result, any{primaryTitle.language}{'en'} is equivalent to just 'en'. Your def romanLang variable is thus always true.
:idea: Please read the FAQ and How to Request Help.
Post Reply