Temporary field rewrite rules

Greetings,

tldr: Musings on a possible feature for alternatives, eventually realizing it wasn’t worth the hassle of doing at all, feel free to ignore this :slight_smile:

I’m looking at the possibility of per-alternative rewrite rules for the alternatives plugin, but am realizing it’s not exactly simple. The rewrite plugin simply adds template fields that rewrite the fixed values, which is nice and clean… of course, trying to do that temporarily rather than globally is not so easy.

If I only cared about directly used fields, I can fairly easily construct the path formats with a subclass of Template that replaces values with a ChainMap to rewrite values before getting to the item’s FormattedItemMapping, but that doesn’t help fields looked up in other contexts, such as other plugin template fields.

The only other option I can think of, other than just giving up on this idea entirely (which may be best after all!) would be to replace/wrap the Item object in a custom one that either overrides the lookup or the _getters, or temporarily modify the item object directly. Both are ugly and messy, the former is tough to pull off and would both slow it down and raise memory pressure, and the latter is racy as hell unless I can guarantee no one else is using the item — admittedly this is probably the case, but still.

It looks like the temporary-item-modification approach does do the job, but also imposes overhead even in cases where the current template lookup won’t be affected by the rewritten fields anyway.

I think I’m going to give up on this, it was another attempt to make complex path formats more maintainable by allowing for selective tweaks to ease navigation on external trees meant for media players, but it’s more trouble than it’s worth. I’m only posting this in case someone else ever tries something similar, for posterity :slight_smile:

This is an interesting line of inquiry! I think it’s worth pondering a bit more, if you’re interested… maybe we could start off with an example or two? It sounds like the idea is that you’d like to have a single set of path formats but to treat them differently for (for example) different formats. Is that right? Maybe an example of a given set of path formats would help illustrate the high-level goal?

I’ll try to clarify my original intention to start. I have a music library where I like it to be stored in a particular layout, which makes sense to me from a sort of otological perspective, but which nonetheless isn’t really convenient to navigate when I actually want to listen to the music.

For example, I like a simple layout of artist and album for storage for posterity, but in reality I don’t find it nice to navigate when you have a ton of single tracks from a ton of albums, that’s a lot of depth to navigate for single tracks. I also like to store my library organized by where I acquired the music. Amazon, iTunes, etc, as it eases re-downloading of any music which is missing or corrupt, but of course that’s not something I care about when I want to listen to it.

I also use a media player device which supports high resolution lossless audio, which has a small lcd, so there are quirks to that, for example, I like to shorten longer album, media, and artist names to ease navigation on such a small display, but I’d never want to do that to the long term library storage.

So, I heavily utilize the beets-alternatives plugin in symlink mode to set up the alternate view(s) into the library. Something like rewrite would be much more convenient than using template functions everywhere in the alternative path formats, but I can’t use rewrite itself, as that also changes the default path formats.

Here’s an incomplete subsection of my config prior to my conversion over into using a plugin instead of path formats. You’ll note heavy use of my plugins to attempt to make the queries and formats more maintainable. In the end, shifting it all into a plugin is probably best anyway, so I’m not sure this line of inquiry needs to go anywhere. But I hope you see the intention — I wanted to change how fields like ‘artist’ were used, but only in the alternatives path formats, not the default path formats. But, in the end, calling into a template function wasn’t that onerous, and the path format evaluation performance meant the plugin approach was best.

album_queries:
  is_game: 'avmedia:"Video Games"'
  is_incomplete_album: 'album_query:is_game missing:2.. , ^album_query:is_game missing:1..'
  is_sampler: 'albumtypes:sampler , album:sampler'
  # This is used both in an alias command and modifyonimport
  is_various_not_comp: 'comp:0 albumartist:@"Various Artists"'

item_queries:
  # Used by the sole-tracks script
  is_mfp: 'label:"Music for Programming"'
  is_ocremix: 'label:"OverClocked ReMix"'
  sole_track_candidates: 'query:is_music \^query:is_mfp \^query:is_ocremix \^query:is_game'

  # As-needed
  albums_to_split_up: 'query:is_incomplete_album existing:..4'
  is_incomplete_album: 'query:is_game missing:2.. , ^query:is_game missing:1..'
  is_loved: album_loved:true , loved:true query:for_single_tracks

  # General and Path Formats. A number of these are used by the `kergoth` plugin for $navigation_path.
  for_single_tracks: 'singleton:true , single_track:true'
  is_christmas: 'genre:Christmas'
  is_classical: 'genre:Classical'
  is_game: 'avmedia:"Video Games"'
  is_music: '^query:is_non_music'
  is_non_music: 'genre:speech , genre:meditation , genre:dharma , genre:book , genre:spoken , genre:background , albumtypes:spokenword , albumtypes:audiobook , albumtypes:"audio drama" , albumtypes:interview'
  is_sampler: 'albumtypes:sampler , album:sampler'
  is_sole_track: 'sole_track:true'
  is_soundtrack: 'albumtypes:soundtrack , genre:soundtrack ^query:is_sampler'

  # Categories
  alt_game_extra: '^albumtypes:soundtrack query:is_game'
  alt_game: 'query:is_game albumtypes:soundtrack , query:is_game ^mediatitle:^'
  alt_to_listen: 'to_listen:true'
  by_label_flat: 'label:"Music for Programming"'
  chiptune_game: 'genre:chiptune query:is_game query:is_soundtrack'
  chiptune: 'genre:chiptune'
  christmas_sole_tracks: 'genre:Christmas query:is_sole_track'
  christmas: 'query:is_christmas'
  classical_sole_tracks: 'genre:classical query:is_sole_track'
  classical: 'query:is_classical ^query:is_soundtrack'
  non_music: 'genre:meditation , genre:background , albumtypes:spokenword , albumtypes:audiobook , albumtypes:"audio drama" , albumtypes:interview'
  sampler: 'query:is_sampler ^query:for_single_tracks'
  soundtrack: 'query:is_soundtrack ^query:for_single_tracks'

item_formats:
  # Unasciified, credited artist
  path_filename: '%if{$album,$disc_and_track_pre}%if{$comp,%if{$artist_credit,$artist_credit,$artist} - }$full_title'

  # Path format components
  artist_title: '%the{$path_artist} - $full_title'
  path_artist: '%asciify{%replace{artist,$artist}}'
  full_title: '$title%if{$e_advisory,$explicit_or_clean}'
  comp_filename: '%if{$album,$disc_and_track_pre}%if{$comp,$path_artist - }$full_title'

  albumsuffix: '%if{$e_albumadvisory, (Explicit)}'
  albumname: '%if{$album,$album%aunique{}$albumsuffix,Single Tracks}'
  albumartistname: '%if{$comp,Compilations,%if{$classical,%if{$album_composer,$album_composer,$albumartist},$albumartist}}'
  artistname: '%if{$classical,%if{$composer,$composer,$artist},$artist}'
  franchisename: '%replace{franchise,$franchise} Franchise'
  gamename: '$game%ifdef{gamedisambig, [$gamedisambig]}%if{$album,$albumsuffix}'

  # Directories
  albumartistdir: '%the{%asciify{%replace{artist,$albumartistname}}}'
  artistdir: '%the{%asciify{%replace{artist,$artistname}}}'
  albumonlydir: '%replace{album,%ifdef{$game,$gamename,$albumname}}'
  franchisedir: '%the{$franchisename}'
  albumdir: '%the{%ifdef{franchise,%path{$franchisedir,$albumonlydir},$albumonlydir}}'

  # Layouts
  by_album: '%path{$albumdir,$comp_filename}'
  bucket_by_album: '%if{$for_single_tracks,%path{Single Tracks,$artist_title},%path{%bucket{$albumdir,alpha},$by_album}}'
  by_artist: '%if{$for_single_tracks,%path{$artistdir,Single Tracks,$full_title},%path{$albumartistdir,$by_album}}'
  bucket_by_artist: '%path{%bucket{%if{$for_single_tracks,$artistdir,$albumartistdir},alpha},$by_artist}'
  bucket_by_label_flat: '%path{%bucket{$label,alpha},$label,$artist_title}'

alternatives:
  formatted:
    directory: '../Alternatives/Formatted'
    formats: link
    query: ^disliked:true
    paths:
      query:loved_single_tracks: 'Loved/Single Tracks/%replace{dap,$artist_title}'
      'genre:Chiptune album_loved:true': 'Loved/Chiptunes/%replace{dap,%if{$for_single_tracks,Single Tracks/$artist_title,$media_albumdir/$comp_filename}}'
      'query:is_game album_loved:true': 'Loved/Games/%replace{dap,%if{$for_single_tracks,Single Tracks/$artist_title,$media_albumdir/$comp_filename}}'
      'album_loved:true query:soundtrack': 'Loved/Soundtracks/%replace{dap,$albumdir/$comp_filename}'
      album_loved:true: 'Loved/Albums/%replace{dap,$albumdir/$comp_filename}'

      to_listen:true: 'To Listen/%replace{dap,%if{$for_single_tracks,Single Tracks/$artist_title,$media_albumdir/$comp_filename}}'

      query:is_non_music: 'NonMusic/%replace{dap,$genre/$albumartistdir/$albumdir/$comp_filename}'
      query:is_mfp: 'Music/%bucket{$label,alpha}/%replace{dap,$label/$full_title}'

      'query:is_sole_track query:classical': '%replace{dap,Classical/Single Tracks/$artist_title}'
      query:classical: 'Classical/%replace{dap,%if{$for_single_tracks,$artistdir/Single Tracks/$full_title,$albumartistdir/$albumdir/$comp_filename}}'

      'genre:chiptune query:alt_game': 'Chiptunes/Games/%replace{dap,%ifdef{franchise,%bucket{$franchisedir,alpha}/$franchisedir/,%bucket{$media_albumdir,alpha}/}$media_albumdir/$comp_filename}'
      'genre:chiptune': 'Chiptunes/Music/%replace{dap,%if{$for_single_tracks,$artistdir/Single Tracks/$full_title,$albumartistdir/$albumdir/$comp_filename}}'

      'query:alt_game ^franchise:^': 'Games/%replace{dap,%bucket{$franchisedir,alpha}/$franchisedir/%if{$for_single_tracks,Single Tracks/$artist_title,$media_albumdir/$comp_filename}}'
      query:alt_game: 'Games/%replace{dap,%if{$for_single_tracks,Single Tracks/$artist_title,%bucket{$media_albumdir,alpha}/$media_albumdir/$comp_filename}}'
      query:is_game: 'Games/Extras/%replace{dap,%ifdef{franchise,$franchisedir/}%if{$for_single_tracks,Single Tracks/$artist_title,$media_albumdir/$comp_filename}}'

      'query:is_sole_track query:is_christmas': 'Christmas/%replace{dap,Single Tracks/$artist_title}'
      query:is_christmas: 'Christmas/%replace{dap,%if{$for_single_tracks,$artistdir/Single Tracks/$full_title,$albumartistdir/$albumdir/$comp_filename}}'

      query:is_sole_track: 'Music/Single Tracks/%replace{dap,$source/$artist_title}'
      query:soundtrack: 'Soundtracks/%replace{dap,$albumdir/$comp_filename}'
      query:sampler: 'Samplers/%replace{dap,$albumdir/$comp_filename}'
      comp:1: '%replace{dap,%if{$for_single_tracks,Music/%bucket{$artistdir,alpha}/$artistdir/Single Tracks/$full_title,Music/Compilations/$albumdir/$comp_filename}}'
      default: '%replace{dap,%if{$for_single_tracks,Music/%bucket{$artistdir,alpha}/$artistdir/Single Tracks/$full_title,Music/%bucket{$albumartistdir,alpha}/$albumartistdir/$albumdir/$comp_filename}}'

Here’s some of the actual replacements that I only apply to the alternatives formats, for example:

replacefunc:
  # These replacements improve the visuals and navigation on the media player
  alt:
    # Marks I don't care about
    ' *[™®©](?![a-zA-Z])': ''

    # Visual cleanup
    '[\x00-\x1f]': _
    '^\s+': ''
    '\s+$': ''

    # Revert asciify for certain chars
    '\.\.\.': '…'

    # Bad font rendering for these characters on the Shanling M0 DAP
    '“': '"'
    '”': '"'
    '‘': "'"
    '’': "'"

  album:
    # Consistency
    'Computec Edition Vol. 1': 'Computec Edition, Vol. 1'
    'HGTV / Paste': 'HGTV & Paste'
    'Kentucky Route Zero, Act II': 'Kentucky Route Zero - Act II'
    'Video Games Live, Volume One': 'Video Games Live: Level 1'

  artist:
    # Deal with non-alphanumeric sort issues when browsing
    '_ensnare_': 'ensnare'
    ':wumpscut:': 'wumpscut'
    '“Weird Al” Yankovic': 'Weird Al Yankovic'
    '\.mpegasus': 'mpegasus'

    # Remove featuring artists for navigation
    ' ([fF]([eE][aA])?[tT]\.|w[//]) .*': ''

    # Multi-artist
    ' / .*': ''
    ' *[,;](?! *Jr).*': ''
    'Chris Ballew and .*': 'Chris Ballew'

    # Consistency
    'George Alistair Sanger': 'George Sanger'

    # Ease navigation
    'Amanda Palmer \+ The Grand Theft Orchestra': 'Amanda Palmer'
    'Amanda Palmer & The Grand Theft Orchestra': 'Amanda Palmer'
    'Bob Seger & the Silver Bullet Band': 'Bob Seger'
    'Ben Harper With Charlie Musselwhite': 'Ben Harper'
    'Bob Marley & The Wailers': 'Bob Marley'
    'Stafford Bawler + Todd Baker': 'Stafford Bawler'

  franchise:
    'The Legend of Zelda': 'Zelda'

  media:
    'Command & Conquer: Red Alert': 'Red Alert'
    'Quest for Glory: Shadows of Darkness': 'Quest for Glory IV'
    'The Chronicles of Riddick: Escape From Butcher Bay': 'Riddick'
    'The Elder Scrolls V: Skyrim': 'Skyrim'
    'The Incredible Adventures of Van Helsing': 'Van Helsing'

To sum up, it was an interesting exercise to imagine how to temporarily alter how fields evaluate in a particular context, but not everywhere, but I don’t think it’s actually necessary to support it, and a non-performant version could be hacked together anyway by passing a wrapper object into functemplate :slight_smile:

Ah, thanks for clarifying!!! This really does make me understand the overall issue. Namely, it’s bound up in the main idea in beets-alternatives: you get to have a whole second tree, organized in a totally different way, that just symlinks to the original files. And for convenience, it would be nice if you could change what some fields “mean” in the context of each set of path formats—for example, maybe you want the $artist field to show up differently rather than manually defining separate fields.

I admit I don’t have any deep insights that would make this easy. But one idea it does make me think of is just trying to add a feature to rewrite that generates new fields instead of changing old ones, perhaps by adding a suffix. So, like, a rewrite rule for $artist could generate $artist_alt instead of changing $artist directly. Then, at least, the different path formats would just need to sprinkle in that suffix everywhere. Not perfectly seamless, but maybe an improvement?

It’s not a bad idea, but honestly not much different than what I’m already doing with external plugins, if I combine a replace/rewrite function with savedformats, so I don’t think it’s worth the hassle of implementing. Ex.:

item_formats:
  artist_alt: '%replace{artist,$artist}'

If someone really likes the rewrite syntax better than replace they can always add a plugin that adds a %rewrite function :slight_smile:

1 Like