test media filename parsing patterns

This commit is contained in:
ngfchl
2026-07-18 18:27:11 +08:00
parent 49fefcfe1f
commit 130fac8259
3 changed files with 91 additions and 9 deletions
+27 -7
View File
@@ -331,8 +331,15 @@ class MediaLibraryItem {
);
}
factory MediaLibraryItem.fromFile(String libraryID, CloudFile file) {
final parsed = ParsedMediaName.parse(file.name);
factory MediaLibraryItem.fromFile(
String libraryID,
CloudFile file, {
String? directoryName,
}) {
final parsed = ParsedMediaName.parse(
file.name,
directoryName: directoryName,
);
final kind = parsed.isEpisode ? TMDBMediaKind.tv : TMDBMediaKind.movie;
return MediaLibraryItem(
libraryID: libraryID,
@@ -506,7 +513,7 @@ class ParsedMediaName {
this.dynamicRange,
});
factory ParsedMediaName.parse(String name) {
factory ParsedMediaName.parse(String name, {String? directoryName}) {
final stem = name.replaceFirst(RegExp(r'\.[^.]+$'), '');
final normalized = stem.replaceAll(RegExp(r'[._]+'), ' ');
final episodeMatch = RegExp(
@@ -558,14 +565,27 @@ class ParsedMediaName {
.replaceAll(RegExp(r'[\[\]【】(){}]'), ' ')
.replaceAll(RegExp(r'\s+'), ' ')
.trim();
final genericName =
title.isEmpty ||
RegExp(r'^\d+$').hasMatch(title) ||
RegExp(r'^S\d{1,2}E\d{1,3}$', caseSensitive: false).hasMatch(title);
ParsedMediaName? parent;
if (genericName &&
directoryName != null &&
directoryName.trim().isNotEmpty) {
parent = ParsedMediaName.parse(directoryName);
if (parent.title.isNotEmpty) title = parent.title;
}
if (title.isEmpty) title = normalized.trim();
return ParsedMediaName(
title: title,
year: yearMatch == null ? null : int.tryParse(yearMatch.group(1)!),
season: season,
year: yearMatch == null
? parent?.year
: int.tryParse(yearMatch.group(1)!),
season: season ?? parent?.season,
episode: episode,
isEpisode: season != null && episode != null,
isEpisode: (season ?? parent?.season) != null && episode != null,
resolution: first(r'\b(?:2160p|1080p|720p|480p|4k)\b'),
source: first(
r'\b(?:WEB[- ]?DL|WEBRip|BluRay|BDRip|REMUX|HDTV|DVD|UHD)\b',
@@ -574,7 +594,7 @@ class ParsedMediaName {
audio: first(
r'\b(?:Atmos|TrueHD|DTS(?:-HD)?|DDP?(?: ?[0-9.]+)?|AAC|FLAC)\b',
),
dynamicRange: first(r'\b(?:HDR10?\+?|Dolby[ .-]?Vision|DV)\b'),
dynamicRange: first(r'(?:HDR10?\+?|HDR|Dolby[ .-]?Vision|DV)'),
);
}
}
+17 -2
View File
@@ -318,7 +318,11 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
Future<void> worker() async {
while (!_cancelScan && next < pending.length) {
final file = pending[next++];
final fallback = MediaLibraryItem.fromFile(library.id, file);
final fallback = MediaLibraryItem.fromFile(
library.id,
file,
directoryName: _parentDirectoryName(file.cloudPath),
);
final existing = unique[file.id];
var item = existing == null
? await _recognizeMediaItem(
@@ -656,7 +660,10 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (_api == null || item.tmdbID == null || item.mediaKind == null) {
return item;
}
final parsed = ParsedMediaName.parse(item.file.name);
final parsed = ParsedMediaName.parse(
item.file.name,
directoryName: _parentDirectoryName(item.file.cloudPath),
);
final extension = _extensionOf(item.file.name);
final year = item.year.isEmpty ? '' : '.${item.year}';
final episode = item.mediaKind == TMDBMediaKind.tv && parsed.isEpisode
@@ -742,6 +749,14 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
return index <= 0 ? '' : value.substring(index + 1).toLowerCase();
}
String? _parentDirectoryName(String cloudPath) {
final values = cloudPath
.split(RegExp(r'[\\/]'))
.where((part) => part.isNotEmpty)
.toList();
return values.length < 2 ? null : values[values.length - 2];
}
String _safeCloudName(String value) {
final cleaned = value
.replaceAll(RegExp(r'[\\/:*?"<>|\x00-\x1F]'), ' ')