Skip to content

Commit

Permalink
feat: auto extract download info for nightcore videos
Browse files Browse the repository at this point in the history
by relying on description, and add `genre` field for downloads
  • Loading branch information
MSOB7YY committed Oct 23, 2024
1 parent 68c7f88 commit 884c126
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ A Beautiful and Feature-rich Music & Video Player with Youtube Support, Built in
- `video_title`, `fulltitle`: video full title
- `title`: extracted music title from video title (*Navjaxx - **<ins>Fading Light</ins>** (Slowed)*)
- `artist`: extracted music artist from video title (***<ins>Navjaxx</ins>** - Fading Light (Slowed)*) or else `channel`
- `genre`: music genre. automatically set to ***Nightcore*** when the video title contains "nightcore".
- `ext`: format container extension (mp4, m4a, webm). this is not necessary as it would be added automatically
- `channel_fulltitle`: channel full name
- `channel`, `uploader`: channel name (excluding ` - Topic`)
Expand Down
4 changes: 3 additions & 1 deletion lib/controller/settings.youtube.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ part of 'settings_controller.dart';
class _YoutubeSettings with SettingsFileWriter {
_YoutubeSettings._internal();

static const _defaultFilenameBuilder = '[%(playlist_autonumber)s] %(video_title)s [(%(channel)s)].%(ext)s';

final ytVisibleShorts = <YTVisibleShortPlaces, bool>{}.obs;
final ytVisibleMixes = <YTVisibleMixesPlaces, bool>{}.obs;
final showChannelWatermarkFullscreen = true.obs;
Expand All @@ -21,7 +23,7 @@ class _YoutubeSettings with SettingsFileWriter {
final onYoutubeLinkOpen = OnYoutubeLinkOpenAction.alwaysAsk.obs;
final tapToSeek = YTSeekActionMode.expandedMiniplayer.obs;
final dragToSeek = YTSeekActionMode.all.obs;
final downloadFilenameBuilder = ''.obs;
final downloadFilenameBuilder = _defaultFilenameBuilder.obs;
final initialDefaultMetadataTags = <String, String>{};

bool markVideoWatched = true;
Expand Down
76 changes: 67 additions & 9 deletions lib/youtube/controller/yt_filename_rebuilder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class _YtFilenameRebuilder {
'video_title', // 'fulltitle'
'title',
'artist',
'genre',
'ext',
'channel_fulltitle',
'channel', // 'uploader'
Expand Down Expand Up @@ -106,20 +107,38 @@ class _YtFilenameRebuilder {
'video_title' || 'fulltitle' => pageResult?.videoInfo?.title ?? videoItem?.title ?? streams?.info?.title,
'title' => () {
final fulltitle = pageResult?.videoInfo?.title ?? videoItem?.title ?? streams?.info?.title;
return fulltitle?.splitArtistAndTitle().$2?.keepFeatKeywordsOnly() ?? streams?.info?.title ?? fulltitle;
final splitted = fulltitle?.splitArtistAndTitle();

final possibleExtracted = _extractArtistTitleFromDescriptionIfNecessary(splitted, (infos) => infos.$2?.keepFeatKeywordsOnly(), streams, pageResult, videoItem);
if (possibleExtracted != null) return possibleExtracted;

return splitted?.$2?.keepFeatKeywordsOnly() ?? streams?.info?.title ?? fulltitle;
}(),
'artist' => () {
// tries extracting artist from video title, or else uses channel name
final fulltitle = pageResult?.videoInfo?.title ?? videoItem?.title ?? streams?.info?.title;

if (fulltitle != null) {
final splitted = fulltitle.splitArtistAndTitle();
if (splitted.$1 != null) return splitted.$1;
final possibleExtracted = _extractArtistTitleFromDescriptionIfNecessary(splitted, (infos) => infos.$1, streams, pageResult, videoItem);
if (possibleExtracted != null) return possibleExtracted;
final String? artist = splitted.$1;
if (artist != null) return artist;
} else {
final possibleExtracted = _extractArtistTitleFromDescriptionIfNecessary(null, (infos) => infos.$1, streams, pageResult, videoItem);
if (possibleExtracted != null) return possibleExtracted;
}

final fullChannelName = pageResult?.channelInfo?.title ?? videoItem?.channel.title ?? streams?.info?.channelName;
return fullChannelName == null ? null : _removeTopicKeyword(fullChannelName);
}(),
'genre' => () {
final fulltitle = pageResult?.videoInfo?.title ?? videoItem?.title ?? streams?.info?.title;
if (fulltitle != null && fulltitle.contains(RegExp('nightcore', caseSensitive: false))) {
return 'Nightcore';
}
return null;
}(),
'ext' => videoStream?.codecInfo.container ?? audioStream?.codecInfo.container ?? 'mp4',
'channel_fulltitle' => pageResult?.channelInfo?.title ?? videoItem?.channel.title ?? streams?.info?.channelName,
'uploader' || 'channel' => () {
Expand All @@ -141,13 +160,7 @@ class _YtFilenameRebuilder {
'view_count' =>
streams?.info?.viewsCount?.toString() ?? pageResult?.videoInfo?.viewsCount?.toString() ?? pageResult?.videoInfo?.viewsText ?? videoItem?.viewsCount?.toString(),
'like_count' => pageResult?.videoInfo?.engagement?.likesCount?.toString() ?? pageResult?.videoInfo?.engagement?.likesCountText,
'description' => () {
final parts = pageResult?.videoInfo?.description?.parts;
if (parts != null && parts.isNotEmpty) {
return _formatDescription(parts);
}
return pageResult?.videoInfo?.description?.rawText ?? streams?.info?.availableDescription ?? videoItem?.availableDescription;
}(),
'description' => _getDescription(streams, pageResult, videoItem),
'duration' => (videoStream?.duration?.inSeconds ?? audioStream?.duration?.inSeconds ?? streams?.info?.durSeconds ?? videoItem?.durSeconds)?.toString(),
'duration_string' => () {
final durSeconds = videoStream?.duration?.inSeconds ?? audioStream?.duration?.inSeconds ?? streams?.info?.durSeconds ?? videoItem?.durSeconds;
Expand All @@ -167,6 +180,51 @@ class _YtFilenameRebuilder {
};
}

String? _getDescription(
VideoStreamsResult? streams,
YoutiPieVideoPageResult? pageResult,
StreamInfoItem? videoItem,
) {
final parts = pageResult?.videoInfo?.description?.parts;
if (parts != null && parts.isNotEmpty) {
return _formatDescription(parts);
}
return pageResult?.videoInfo?.description?.rawText ?? streams?.info?.availableDescription ?? videoItem?.availableDescription;
}

T? _extractArtistTitleFromDescriptionIfNecessary<T>(
(String? artist, String? title)? info,
T? Function((String? artist, String? title) infos) onMatch,
VideoStreamsResult? streams,
YoutiPieVideoPageResult? pageResult,
StreamInfoItem? videoItem,
) {
if ((info?.$1 == null && info?.$2 == null) || info?.$1?.toLowerCase() == 'nightcore') {
final description = _getDescription(streams, pageResult, videoItem);
if (description != null) {
final title = info?.$2?.splitFirst('(').splitFirst('[');
final regex = title == null ? RegExp('(song|info|details)\\W+(.+)', caseSensitive: false) : RegExp('(song|info|details)?\\W+(.+$title.+)', caseSensitive: false);
final regexArtist = RegExp('artist:(.*)', caseSensitive: false);
final regexTitle = RegExp('title:(.*)', caseSensitive: false);
for (final line in description.split('\n')) {
final m = regex.firstMatch(line);
try {
var infosLine = m?.group(2)?.splitArtistAndTitle();
if (infosLine == null) {
final fallback = (regexArtist.firstMatch(line)?.group(1)?.trim(), regexTitle.firstMatch(line)?.group(1)?.trim());
if (fallback.$1 != null || fallback.$2 != null) infosLine = fallback;
}
if (infosLine != null) {
final resolved = onMatch(infosLine);
if (resolved != null) return resolved;
}
} catch (_) {}
}
}
}
return null;
}

String _removeTopicKeyword(String text) {
const topic = '- Topic';
final startIndex = (text.length - topic.length).withMinimum(0);
Expand Down
1 change: 1 addition & 0 deletions lib/youtube/yt_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ class YTUtils {
else
FFMPEGTagField.artist: YoutubeController.filenameBuilder.buildParamForFilename('channel'),
if (autoExtract) FFMPEGTagField.album: YoutubeController.filenameBuilder.buildParamForFilename('channel'),
if (autoExtract) FFMPEGTagField.genre: YoutubeController.filenameBuilder.buildParamForFilename('genre'),
FFMPEGTagField.title: YoutubeController.filenameBuilder.buildParamForFilename('title'),
FFMPEGTagField.artist: YoutubeController.filenameBuilder.buildParamForFilename('artist'),
FFMPEGTagField.album: YoutubeController.filenameBuilder.buildParamForFilename('channel'),
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: namida
description: A Beautiful and Feature-rich Music Player, With YouTube & Video Support Built in Flutter
publish_to: "none"
version: 4.5.7-beta+241023232
version: 4.5.75-beta+241023236

environment:
sdk: ">=3.4.0 <4.0.0"
Expand Down

0 comments on commit 884c126

Please sign in to comment.