11# This file is part of beets.
2- # Copyright 2016, Jan-Erik Dahlin
2+ # Copyright 2016, Jan-Erik Dahlin, Henry Oberholtzer.
33#
44# Permission is hereby granted, free of charge, to any person obtaining
55# a copy of this software and associated documentation files (the
1616(possibly also extract track and artist)
1717"""
1818
19+ from __future__ import annotations
20+
1921import re
2022from collections .abc import Iterator , MutableMapping , ValuesView
2123from datetime import datetime
2224from functools import cached_property
2325from pathlib import Path
26+ from typing import TYPE_CHECKING
2427
2528from beets import config
26- from beets .importer import ImportSession , ImportTask
27- from beets .library import Item
2829from beets .plugins import BeetsPlugin
2930from beets .util import displayable_path
3031
32+ if TYPE_CHECKING :
33+ from beets .importer import ImportSession , ImportTask
34+ from beets .library import Item
35+
3136# Filename field extraction patterns
3237RE_TRACK_INFO = re .compile (
3338 r"""
5257 re .VERBOSE | re .IGNORECASE ,
5358)
5459
60+ RE_ALPHANUM_INDEX = re .compile (r"^[A-Z]{1,2}\d{,2}\b" )
61+
5562# Catalog number extraction pattern
5663RE_CATALOGNUM = re .compile (
5764 r"""
@@ -180,14 +187,14 @@ def filename_task(self, task: ImportTask, session: ImportSession) -> None:
180187 Once the information has been obtained and checked, it
181188 is applied to the items to improve later metadata lookup.
182189 """
183- # Create the list of items to process
190+ # Retrieve the list of items to process
184191
185192 items : list [Item ] = task .items
186193
187194 # If there's no missing data to parse
188195 if not self ._check_missing_data (items ):
189196 return
190-
197+ # Retrieve the path characteristics to check
191198 parent_folder , item_filenames = self ._get_path_strings (items )
192199
193200 album_matches = self ._parse_album_info (parent_folder )
@@ -259,6 +266,8 @@ def _build_track_matches(
259266 self , item_filenames : dict [Item , str ]
260267 ) -> dict [Item , FilenameMatch ]:
261268 track_matches : dict [Item , FilenameMatch ] = {}
269+ # Check for alphanumeric indices
270+ self ._parse_alphanumeric_index (item_filenames )
262271 for item , filename in item_filenames .items ():
263272 if m := self ._check_user_matches (filename , self .file_patterns ):
264273 track_matches [item ] = m
@@ -267,6 +276,48 @@ def _build_track_matches(
267276 track_matches [item ] = match
268277 return track_matches
269278
279+ @staticmethod
280+ def _parse_alphanumeric_index (item_filenames : dict [Item , str ]) -> None :
281+ """Before continuing to regular track matches, see if an alphanumeric
282+ tracklist can be extracted. "A1, B1, B2" Sometimes these are followed
283+ by a dash or dot and must be anchored to the start of the string.
284+
285+ All matched patterns are extracted, and replaced with integers.
286+
287+ Discs are not accounted for.
288+ """
289+
290+ def match_index (filename : str ) -> str :
291+ m = RE_ALPHANUM_INDEX .match (filename )
292+ if not m :
293+ return ""
294+ else :
295+ return m .group ()
296+
297+ # Extract matches for alphanumeric indexes
298+ indexes : list [tuple [str , Item ]] = [
299+ (match_index (filename ), item )
300+ for item , filename in item_filenames .items ()
301+ ]
302+ # If all the tracks do not start with a vinyl index, abort
303+ if not all ([i [0 ] for i in indexes ]):
304+ return
305+
306+ # Utility function for sorting
307+ def index_key (x : tuple [str , Item ]):
308+ return x [0 ]
309+
310+ # If all have match, sort by the matched strings
311+ indexes .sort (key = index_key )
312+ # Iterate through all the filenames
313+ for index , pair in enumerate (indexes ):
314+ match , item = pair
315+ # Substitute the alnum index with an integer
316+ new_filename = item_filenames [item ].replace (
317+ match , str (index + 1 ), 1
318+ )
319+ item_filenames [item ] = new_filename
320+
270321 @staticmethod
271322 def _parse_track_info (text : str ) -> FilenameMatch :
272323 match = RE_TRACK_INFO .match (text )
0 commit comments