Skip to content

Commit f7ca034

Browse files
authored
Merge pull request #47 from DakaraProject/feature/detect-instrumental-files
Improve management of instrumental files
2 parents ef7f616 + 28e2807 commit f7ca034

10 files changed

Lines changed: 359 additions & 83 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@
3333
### Changed
3434

3535
- The metadata parser can now be specified with the configuration key `metadata_parser`. Accepted values are `ffprobe` (default), `mediainfo`, or `null` (for testing only).
36+
- Songs with instrumental version are specified either with the `instrumental_file` field, if there is an audio file of the same name in the same directory as the video file, or with the `instrumental_track` field, if the video file has a second audio track. Both fields are sent to the server, instead of the `has_instrumental` field (see removed sub-section).
3637

3738
### Removed
3839

3940
- Dropped Python 3.9.
41+
- Removed the `has_instrumental` field of songs, in favor of `instrumental_file` and `instrumental_track`.
42+
43+
### Fixed
44+
45+
- Matroshka files containing only an audio track and having the `.mka` extension are correctly detected as adio files.
4046

4147
## 1.9.0 - 2025-03-06
4248

src/dakara_feeder/directory.py

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"""List directoryes to extract song files."""
22

33
import logging
4+
import mimetypes
45
from itertools import groupby
56

67
import filetype
78

9+
from dakara_feeder.media import is_audio, is_video
810
from dakara_feeder.subtitle.parsing import is_subtitle
911

1012
logger = logging.getLogger(__name__)
@@ -54,23 +56,81 @@ def get_path_without_extension(path):
5456

5557

5658
def get_main_type(file):
57-
"""Get the first part of the MIME type of the given file.
59+
"""Get the type of a given file.
60+
61+
First try a manual detection on expected extensions, as this is required
62+
for subtitles, and as some audio files can be mistaken for video files
63+
(notably, `.mka` files). Then try an automatic detection based on the
64+
extension of the file. If this fails, try an automatic detection based on
65+
the magic numbers in the file.
5866
5967
Args:
60-
file (pathlib.Path): Absolute path to the file to extract the MIME type.
68+
file (pathlib.Path): Absolute path to the file to extract the type.
6169
6270
Returns
63-
str: Main type if the MIME type can be extracted, `None` otherwise.
71+
str: Type if it can be extracted, `None` otherwise.
6472
"""
65-
kind = filetype.guess(str(file))
73+
# manual detection of video files
74+
if is_video(file):
75+
return "video"
76+
77+
# manual detection of audio files
78+
if is_audio(file):
79+
return "audio"
80+
81+
# manual detection of subtitle files
82+
if is_subtitle(file):
83+
return "subtitle"
6684

67-
if not kind:
85+
# automatic detection using extension
86+
mimetype = get_mimetype_by_extension(file)
87+
88+
# if it failed, automatic detection using magic number
89+
if mimetype is None:
90+
mimetype = get_mimetype_by_magic_number(file)
91+
92+
# abort if not found
93+
if mimetype is None:
6894
return None
6995

70-
maintype, _ = kind.mime.split("/")
96+
# extract main type from MIME type
97+
maintype, _ = mimetype.split("/")
7198
return maintype
7299

73100

101+
def get_mimetype_by_magic_number(file):
102+
"""Return the MIME type of a file based on its magic number.
103+
104+
This strategy is more costly and should be used as a second option.
105+
106+
Args:
107+
file (pathlib.Path): Absolute path to the file to extract the MIME type.
108+
109+
Returns
110+
str: MIME type. `None` if teh MIME type cannot be identified.
111+
"""
112+
instance = filetype.guess(str(file))
113+
114+
if instance:
115+
return instance.mime
116+
117+
return None
118+
119+
120+
def get_mimetype_by_extension(file):
121+
"""Return the MIME type of a file based on its extension.
122+
123+
Args:
124+
file (pathlib.Path): Absolute path to the file to extract the MIME type.
125+
126+
Returns
127+
str: MIME type. `None` if the MIME type cannot be identified.
128+
"""
129+
# TODO Use `guess_type_file` from Python 3.13
130+
mimetype, _ = mimetypes.guess_type(file, strict=False)
131+
return mimetype
132+
133+
74134
def group_by_type(files, path):
75135
"""Group files by extension.
76136
@@ -97,7 +157,7 @@ def group_by_type(files, path):
97157
audios.append(file)
98158
continue
99159

100-
if is_subtitle(file):
160+
if maintype == "subtitle":
101161
subtitles.append(file)
102162
continue
103163

src/dakara_feeder/media.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
VIDEO_EXTENSIONS = (
2+
".mkv",
3+
".mp4",
4+
)
5+
"""Known video extensions."""
6+
7+
AUDIO_EXTENSIONS = (
8+
".ogg",
9+
".mka",
10+
".m4a",
11+
)
12+
"""Known audio extensions."""
13+
14+
15+
def is_video(file):
16+
"""Detect known video extensions.
17+
18+
Args:
19+
file (pathlib.Path): File.
20+
21+
Returns: True if the extension is known to be of a video file.
22+
"""
23+
return file.suffix in VIDEO_EXTENSIONS
24+
25+
26+
def is_audio(file):
27+
"""Detect known video extensions.
28+
29+
Args:
30+
file (pathlib.Path): File.
31+
32+
Returns: True if the extension is known to be of an audio file.
33+
"""
34+
return file.suffix in AUDIO_EXTENSIONS

src/dakara_feeder/song.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ class BaseSong:
2121
2222
- `get_title`;
2323
- `get_duration`;
24-
- `get_has_instrumental`;
24+
- `get_instrumental_file`;
25+
- `get_instrumental_track`;
2526
- `get_version`;
2627
- `get_detail`;
2728
- `get_detail_video`;
@@ -151,20 +152,29 @@ def get_duration(self):
151152
"""
152153
return self.metadata.get_duration().total_seconds()
153154

154-
def get_has_instrumental(self):
155-
"""Get the flag if the song has an instrumental track.
155+
def get_instrumental_file(self):
156+
"""Get the path to the instrumental file, if any.
156157
157158
Returns:
158-
bool: `True` either if there is an extra audio file siding with the
159-
video file, or if the video file has more than 2 audio tracks.
159+
str: Name of the extra audio file siding with the video
160+
file. `None` otherwise.
160161
"""
161162
if self.audio_path:
162-
return True
163+
return self.audio_path.name
163164

165+
return None
166+
167+
def get_instrumental_track(self):
168+
"""Get index of the instrumental track, if any.
169+
170+
Returns:
171+
int: Index of the second audio track (i.e. 1) if the video file has more
172+
than 2 audio tracks. `None` otherwise.
173+
"""
164174
if self.metadata.get_audio_tracks_count() >= 2:
165-
return True
175+
return 1
166176

167-
return False
177+
return None
168178

169179
def get_artists(self):
170180
"""Get the list of artists.
@@ -317,7 +327,8 @@ def get_representation(self):
317327
"filename": str(self.video_path.name),
318328
"directory": get_clean_directory(self.video_path.parent),
319329
"duration": self.get_duration(),
320-
"has_instrumental": self.get_has_instrumental(),
330+
"instrumental_file": self.get_instrumental_file(),
331+
"instrumental_track": self.get_instrumental_track(),
321332
"version": self.get_version(),
322333
"detail": self.get_detail(),
323334
"detail_video": self.get_detail_video(),

tests/integration/test_feeder_songs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def test_feed(self, mocked_http_client_dakara_class):
4444
"filename": "dummy.mkv",
4545
"directory": "",
4646
"duration": 2.023,
47-
"has_instrumental": True,
47+
"instrumental_file": None,
48+
"instrumental_track": 1,
4849
"artists": [],
4950
"works": [],
5051
"tags": [],

tests/resources/filetype/file.m4a

856 Bytes
Binary file not shown.

tests/resources/filetype/file.mka

635 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)