Add metadata generation for feet saber...
This commit is contained in:
@@ -35,7 +35,7 @@ from model.dancerush import DRSSongDifficulty
|
||||
from model.dancerush import DRSTrackPoint
|
||||
from model.dancerush import OUTPUT_ZIP_NAME
|
||||
from util import create_valid_filename
|
||||
from util import get_ogg_and_duration
|
||||
from util import get_drs_ogg_and_duration
|
||||
from util import get_song_cover_path
|
||||
from util import yyyymmdd_to_ticks
|
||||
from util import zipdir
|
||||
@@ -165,7 +165,7 @@ def create_dd_tracks_from_DRSSongData(drs_song_data: DRSSongData, target_dir: st
|
||||
dd_bps = dd_bmp / 60
|
||||
|
||||
folder_path = TRACK_ID_TO_PATH.get(drs_song_data.song_id)
|
||||
song_path, song_length = get_ogg_and_duration(folder_path)
|
||||
song_path, song_length = get_drs_ogg_and_duration(folder_path)
|
||||
if not song_path or not song_length:
|
||||
print(
|
||||
f'No song found for {drs_song_data.info.title_name} ({drs_song_data.song_id})',
|
||||
@@ -205,7 +205,7 @@ def create_dd_tracks_from_DRSSongData(drs_song_data: DRSSongData, target_dir: st
|
||||
)
|
||||
|
||||
total_note_count = len(sphere_notes + line_notes + road_block_notes) # noqa
|
||||
drs_beat_map = DDBeatMap(
|
||||
dd_beat_map = DDBeatMap(
|
||||
data=DDBeatMapData(
|
||||
name=f'{drs_song_data.info.title_name} {attr}',
|
||||
sphereNodes=sphere_notes,
|
||||
@@ -217,19 +217,19 @@ def create_dd_tracks_from_DRSSongData(drs_song_data: DRSSongData, target_dir: st
|
||||
)
|
||||
|
||||
song_paths.append(
|
||||
drs_beat_map.save_to_file(
|
||||
dd_beat_map.save_to_file(
|
||||
target_dir, f'{attr}.json',
|
||||
),
|
||||
)
|
||||
song_paths.append(
|
||||
drs_beat_map.block_less.save_to_file(
|
||||
dd_beat_map.block_less.save_to_file(
|
||||
target_dir, f'{attr}_blockless.json',
|
||||
),
|
||||
)
|
||||
|
||||
normal, normal_no_blocks, easy, easy_no_blocks = song_paths
|
||||
create_ticks = yyyymmdd_to_ticks(str(drs_song_data.info.distribution_date))
|
||||
drs_song_info_json = DDBeatMapInfoFile(
|
||||
drs_beat_map_info = DDBeatMapInfoFile(
|
||||
CreateTicks=create_ticks,
|
||||
CreateTime=str(create_ticks),
|
||||
BeatMapId=drs_song_data.song_id,
|
||||
@@ -244,15 +244,15 @@ def create_dd_tracks_from_DRSSongData(drs_song_data: DRSSongData, target_dir: st
|
||||
DRS_Hard=os.path.basename(easy),
|
||||
DRS_Expert=os.path.basename(normal),
|
||||
)
|
||||
drs_song_info_json.save_to_file(target_dir)
|
||||
drs_beat_map_info.save_to_file(target_dir)
|
||||
|
||||
print(f'Created {drs_song_data.info.title_name} ({drs_song_data.song_id})')
|
||||
return drs_song_info_json
|
||||
return drs_beat_map_info
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Create DD tracks from DRS Song Data',
|
||||
description='Create Dance Dash tracks from DANCERUSH STARDOM Song Data',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--song-id',
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
from model.dancedash import DDBeatMap
|
||||
from model.dancedash import DDBeatMapData
|
||||
from model.dancedash import DDBeatMapInfoFile
|
||||
from model.dancedash import DDLineNode
|
||||
from model.dancedash import DDRoadBlockNode
|
||||
from model.dancedash import DDSphereNode
|
||||
from model.feetsaber import FSBeatMapFile
|
||||
from model.feetsaber import FSInfoDat
|
||||
from util import convert_egg_to_ogg_and_get_length
|
||||
from util import get_first_image_file_in_folder
|
||||
from util import random_10_digit_int
|
||||
from util import yyyymmdd_to_ticks
|
||||
|
||||
|
||||
def map_sphere_nodes(fs_beat_map: FSBeatMapFile) -> list[DDSphereNode]:
|
||||
spheres = []
|
||||
...
|
||||
return spheres
|
||||
|
||||
|
||||
def map_line_nodes(fs_beat_map: FSBeatMapFile) -> list[DDLineNode]:
|
||||
lines = []
|
||||
...
|
||||
return lines
|
||||
|
||||
|
||||
def map_down_and_jump_notes(fs_beat_map: FSBeatMapFile) -> list[DDRoadBlockNode]:
|
||||
road_blocks = []
|
||||
...
|
||||
return road_blocks
|
||||
|
||||
|
||||
def create_dd_tracks_from_fs(fs_map_dir: str) -> DDBeatMapInfoFile:
|
||||
info_file_path = os.path.join(fs_map_dir, 'Info.dat')
|
||||
fs_info = FSInfoDat.from_json_file(info_file_path)
|
||||
|
||||
egg_song_path = [
|
||||
os.path.join(fs_map_dir, f) for f in os.listdir(
|
||||
fs_map_dir,
|
||||
) if f.endswith('.egg')
|
||||
][0]
|
||||
target_dir = f'{fs_info.songName} - {fs_info.levelAuthorName}'
|
||||
if not os.path.exists(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
|
||||
ogg_song_name, song_length = convert_egg_to_ogg_and_get_length(
|
||||
egg_song_path,
|
||||
)
|
||||
|
||||
new_song_path = os.path.join(target_dir, ogg_song_name)
|
||||
shutil.copy(egg_song_path, new_song_path)
|
||||
song_path = new_song_path
|
||||
if song_cover_path := get_first_image_file_in_folder(fs_map_dir):
|
||||
new_song_cover_path = os.path.join(
|
||||
target_dir, os.path.basename(song_cover_path),
|
||||
)
|
||||
shutil.copy(song_cover_path, new_song_cover_path)
|
||||
song_cover_path = new_song_cover_path
|
||||
|
||||
song_paths = []
|
||||
for difficulty_set in fs_info.difficultyBeatmapSets:
|
||||
for difficulty_set in difficulty_set.difficultyBeatmaps:
|
||||
beat_map: FSBeatMapFile = difficulty_set.get_beatmap(fs_map_dir)
|
||||
|
||||
sphere_notes = map_sphere_nodes(beat_map)
|
||||
line_notes = map_line_nodes(beat_map)
|
||||
road_block_notes = map_down_and_jump_notes(beat_map)
|
||||
|
||||
total_note_count = len(sphere_notes + line_notes + road_block_notes) # noqa
|
||||
dd_beat_map = DDBeatMap(
|
||||
data=DDBeatMapData(
|
||||
name=f'{fs_info.songName}',
|
||||
sphereNodes=sphere_notes,
|
||||
lineNodes=line_notes,
|
||||
roadBlockNodes=road_block_notes,
|
||||
),
|
||||
BPM=fs_info.beatsPerMinute,
|
||||
NPS=str(round(total_note_count / song_length, 2)),
|
||||
)
|
||||
|
||||
song_paths.append(
|
||||
dd_beat_map.save_to_file(
|
||||
target_dir, 'a.json',
|
||||
),
|
||||
)
|
||||
|
||||
available_difficulties = [
|
||||
'Easy', 'Normal',
|
||||
'Hard', 'Expert', 'Master', 'ACE',
|
||||
]
|
||||
song_path_difficulties = zip(song_paths, available_difficulties)
|
||||
difficulties = {
|
||||
f'DRS_{d}': os.path.basename(
|
||||
sp,
|
||||
) for sp, d in song_path_difficulties
|
||||
}
|
||||
|
||||
create_ticks = yyyymmdd_to_ticks(datetime.now().strftime('%Y%m%d'))
|
||||
dd_beat_map_info = DDBeatMapInfoFile(
|
||||
CreateTicks=create_ticks,
|
||||
CreateTime=str(create_ticks),
|
||||
BeatMapId=random_10_digit_int(),
|
||||
SongName=fs_info.songName,
|
||||
SongLength=str(song_length),
|
||||
SongAuthorName=fs_info.songAuthorName,
|
||||
LevelAuthorName=fs_info.levelAuthorName,
|
||||
Bpm=str(fs_info.beatsPerMinute),
|
||||
SongPath=os.path.basename(song_path),
|
||||
CoverPath=os.path.basename(song_cover_path or '') or None,
|
||||
**difficulties,
|
||||
)
|
||||
|
||||
dd_beat_map_info.save_to_file(target_dir)
|
||||
|
||||
print(
|
||||
f'Created {dd_beat_map_info.SongName} ({dd_beat_map_info.BeatMapId})',
|
||||
)
|
||||
return dd_beat_map_info
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Create Dance Dash tracks from Feet Saber maps',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--fs-map-dir',
|
||||
type=str,
|
||||
help='The directory containing the Feet Saber map files',
|
||||
required=True,
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
if not os.path.exists(args.fs_map_dir):
|
||||
print(f'Invalid Feet Saber map directory: {args.fs_map_dir}')
|
||||
raise SystemExit(1)
|
||||
|
||||
create_dd_tracks_from_fs(args.fs_map_dir)
|
||||
raise SystemExit(0)
|
||||
@@ -0,0 +1,295 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSEditor:
|
||||
version: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSEditorMetadata:
|
||||
lastEditedBy: str
|
||||
editors: dict[str, FSEditor]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSContributor:
|
||||
role: str
|
||||
name: str
|
||||
iconPath: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSDatCustomData:
|
||||
contributors: list[FSContributor]
|
||||
editors: FSEditorMetadata
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapCustomData:
|
||||
difficultyLabel: str
|
||||
editorOffset: int
|
||||
editorOldOffset: int
|
||||
suggestions: list[str]
|
||||
requirements: list[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatmap:
|
||||
difficulty: str
|
||||
difficultyRank: int
|
||||
beatmapFilename: str
|
||||
noteJumpMovementSpeed: int
|
||||
noteJumpStartBeatOffset: float
|
||||
customData: FSBeatMapCustomData
|
||||
|
||||
def get_beatmap(self, map_dir: str) -> FSBeatMapFile:
|
||||
file_name = os.path.join(map_dir, self.beatmapFilename)
|
||||
beat_map_dict = json.load(open(file_name, encoding='utf-8'))
|
||||
return FSBeatMapFile.from_json_dict(beat_map_dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSDifficultyBeatMapSets:
|
||||
beatmapCharacteristicName: str
|
||||
difficultyBeatmaps: list[FSBeatmap]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSInfoDat:
|
||||
version: str
|
||||
songName: str
|
||||
songSubName: str
|
||||
songAuthorName: str
|
||||
levelAuthorName: str
|
||||
beatsPerMinute: int
|
||||
shuffle: int
|
||||
shufflePeriod: float
|
||||
previewStartTime: int
|
||||
previewDuration: int
|
||||
songFilename: str
|
||||
coverImageFilename: str
|
||||
environmentName: str
|
||||
allDirectionsEnvironmentName: str
|
||||
songTimeOffset: int
|
||||
customData: FSDatCustomData
|
||||
difficultyBeatmapSets: list[FSDifficultyBeatMapSets]
|
||||
|
||||
@classmethod
|
||||
def from_json_dict(cls, json_dict: dict) -> FSInfoDat:
|
||||
custom_data_raw = json_dict['_customData']
|
||||
|
||||
def remove_underscore_from_keys(data):
|
||||
return {key.lstrip('_'): value for key, value in data.items()}
|
||||
|
||||
contributors = [
|
||||
FSContributor(**remove_underscore_from_keys(contributor)) for contributor in
|
||||
custom_data_raw['_contributors']
|
||||
]
|
||||
|
||||
editors = {
|
||||
editor: FSEditor(version=data['version']) for editor, data in custom_data_raw['_editors'].items() if
|
||||
editor != '_lastEditedBy'
|
||||
}
|
||||
|
||||
custom_data = FSDatCustomData(
|
||||
contributors=contributors,
|
||||
editors=FSEditorMetadata(
|
||||
lastEditedBy=custom_data_raw['_editors']['_lastEditedBy'],
|
||||
editors=editors,
|
||||
),
|
||||
)
|
||||
|
||||
difficulty_beatmap_sets_raw = json_dict['_difficultyBeatmapSets']
|
||||
|
||||
difficulty_beatmaps_list = []
|
||||
for dbset in difficulty_beatmap_sets_raw:
|
||||
beatmaps = [
|
||||
FSBeatmap(
|
||||
difficulty=beatmap['_difficulty'],
|
||||
difficultyRank=beatmap['_difficultyRank'],
|
||||
beatmapFilename=beatmap['_beatmapFilename'],
|
||||
noteJumpMovementSpeed=beatmap['_noteJumpMovementSpeed'],
|
||||
noteJumpStartBeatOffset=beatmap['_noteJumpStartBeatOffset'],
|
||||
customData=FSBeatMapCustomData(
|
||||
difficultyLabel=beatmap['_customData']['_difficultyLabel'],
|
||||
editorOffset=beatmap['_customData']['_editorOffset'],
|
||||
editorOldOffset=beatmap['_customData']['_editorOldOffset'],
|
||||
suggestions=beatmap['_customData']['_suggestions'],
|
||||
requirements=beatmap['_customData']['_requirements'],
|
||||
),
|
||||
) for beatmap in dbset['_difficultyBeatmaps']
|
||||
]
|
||||
|
||||
difficulty_beatmaps_list.append(
|
||||
FSDifficultyBeatMapSets(
|
||||
beatmapCharacteristicName=dbset['_beatmapCharacteristicName'],
|
||||
difficultyBeatmaps=beatmaps,
|
||||
),
|
||||
)
|
||||
|
||||
return cls(
|
||||
version=json_dict['_version'],
|
||||
songName=json_dict['_songName'],
|
||||
songSubName=json_dict['_songSubName'],
|
||||
songAuthorName=json_dict['_songAuthorName'],
|
||||
levelAuthorName=json_dict['_levelAuthorName'],
|
||||
beatsPerMinute=json_dict['_beatsPerMinute'],
|
||||
shuffle=json_dict['_shuffle'],
|
||||
shufflePeriod=json_dict['_shufflePeriod'],
|
||||
previewStartTime=json_dict['_previewStartTime'],
|
||||
previewDuration=json_dict['_previewDuration'],
|
||||
songFilename=json_dict['_songFilename'],
|
||||
coverImageFilename=json_dict['_coverImageFilename'],
|
||||
environmentName=json_dict['_environmentName'],
|
||||
allDirectionsEnvironmentName=json_dict['_allDirectionsEnvironmentName'],
|
||||
songTimeOffset=json_dict['_songTimeOffset'],
|
||||
customData=custom_data,
|
||||
difficultyBeatmapSets=difficulty_beatmaps_list,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_json_file(cls, json_file: str) -> FSInfoDat:
|
||||
import json
|
||||
with open(json_file) as f:
|
||||
json_dict = json.load(f)
|
||||
return cls.from_json_dict(json_dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFileBookmark:
|
||||
time: int
|
||||
name: str
|
||||
color: list[float]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFileCustomData:
|
||||
time: float
|
||||
bookmarks: list[FSBeatMapFileBookmark]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFileEvent:
|
||||
time: int
|
||||
type: int
|
||||
value: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFileNoteCustomData:
|
||||
position: tuple[float, float]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFileNote:
|
||||
time: float
|
||||
lineIndex: int
|
||||
lineLayer: int
|
||||
type: int
|
||||
cutDirection: int
|
||||
customData: FSBeatMapFileNoteCustomData
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFileObstacleCustomData:
|
||||
interactable: bool
|
||||
fake: bool
|
||||
position: tuple[float, float]
|
||||
scale: tuple[float, float]
|
||||
color: tuple[float, float, float, float]
|
||||
localRotation: tuple[float, float, float] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFileObstacle:
|
||||
time: float
|
||||
lineIndex: int
|
||||
type: int
|
||||
duration: int
|
||||
width: int
|
||||
customData: FSBeatMapFileObstacleCustomData
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFile:
|
||||
version: str
|
||||
customData: FSBeatMapFileCustomData
|
||||
events: list
|
||||
notes: list
|
||||
obstacles: list
|
||||
waypoints: list
|
||||
|
||||
@classmethod
|
||||
def from_json_dict(cls, json_dict: dict) -> FSBeatMapFile:
|
||||
custom_data = FSBeatMapFileCustomData(
|
||||
time=json_dict['_customData']['_time'],
|
||||
bookmarks=[
|
||||
FSBeatMapFileBookmark(
|
||||
time=bm['_time'],
|
||||
name=bm['_name'],
|
||||
color=bm['_color'],
|
||||
) for bm in json_dict['_customData']['_bookmarks']
|
||||
],
|
||||
)
|
||||
|
||||
events = [
|
||||
FSBeatMapFileEvent(
|
||||
time=event['_time'],
|
||||
type=event['_type'],
|
||||
value=event['_value'],
|
||||
) for event in json_dict['_events']
|
||||
]
|
||||
|
||||
notes = [
|
||||
FSBeatMapFileNote(
|
||||
time=note['_time'],
|
||||
lineIndex=note['_lineIndex'],
|
||||
lineLayer=note['_lineLayer'],
|
||||
type=note['_type'],
|
||||
cutDirection=note['_cutDirection'],
|
||||
customData=FSBeatMapFileNoteCustomData(
|
||||
position=tuple(note['_customData']['_position']),
|
||||
),
|
||||
) for note in json_dict['_notes']
|
||||
]
|
||||
|
||||
obstacles = [
|
||||
FSBeatMapFileObstacle(
|
||||
time=obstacle['_time'],
|
||||
lineIndex=obstacle['_lineIndex'],
|
||||
type=obstacle['_type'],
|
||||
duration=obstacle['_duration'],
|
||||
width=obstacle['_width'],
|
||||
customData=FSBeatMapFileObstacleCustomData(
|
||||
interactable=obstacle['_customData']['_interactable'],
|
||||
fake=obstacle['_customData']['_fake'],
|
||||
position=tuple(obstacle['_customData']['_position']),
|
||||
localRotation=tuple(obstacle['_customData']['_localRotation']) if '_localRotation' in obstacle[
|
||||
'_customData'
|
||||
] else None,
|
||||
scale=tuple(obstacle['_customData']['_scale']),
|
||||
color=tuple(obstacle['_customData']['_color']),
|
||||
),
|
||||
) for obstacle in json_dict['_obstacles']
|
||||
]
|
||||
|
||||
return cls(
|
||||
version=json_dict['_version'],
|
||||
customData=custom_data,
|
||||
events=events,
|
||||
notes=notes,
|
||||
obstacles=obstacles,
|
||||
waypoints=json_dict['_waypoints'],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_json_file(cls, json_file: str) -> FSBeatMapFile:
|
||||
import json
|
||||
with open(json_file) as f:
|
||||
json_dict = json.load(f)
|
||||
return cls.from_json_dict(json_dict)
|
||||
+3
-3
@@ -8,7 +8,7 @@ from datetime import datetime
|
||||
import pytest
|
||||
|
||||
from util import create_valid_filename
|
||||
from util import get_ogg_and_duration
|
||||
from util import get_drs_ogg_and_duration
|
||||
from util import get_song_cover_path
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_get_ogg_and_duration(file_names, expected_output, tmpdir, mocker):
|
||||
|
||||
mocker.patch('subprocess.check_output', return_value='10')
|
||||
|
||||
ogg_file, duration_str = get_ogg_and_duration(str(folder_path))
|
||||
ogg_file, duration_str = get_drs_ogg_and_duration(str(folder_path))
|
||||
|
||||
if ogg_file:
|
||||
ogg_file = os.path.basename(ogg_file)
|
||||
@@ -44,7 +44,7 @@ def test_get_ogg_and_duration_subprocess_error(tmpdir, mocker):
|
||||
side_effect=subprocess.CalledProcessError(1, 'cmd'),
|
||||
)
|
||||
|
||||
ogg_file, duration_str = get_ogg_and_duration(str(folder_path))
|
||||
ogg_file, duration_str = get_drs_ogg_and_duration(str(folder_path))
|
||||
|
||||
assert (ogg_file, duration_str) == (None, None)
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def get_ogg_and_duration(folder_path) -> tuple[str, str] | tuple[None, None]:
|
||||
def get_drs_ogg_and_duration(folder_path) -> tuple[str, str] | tuple[None, None]:
|
||||
ogg_files = [f for f in os.listdir(folder_path) if f.endswith('.ogg')]
|
||||
matching_files = [f for f in ogg_files if f.endswith('clip1.ogg')]
|
||||
if matching_files:
|
||||
@@ -37,6 +38,16 @@ def get_song_cover_path(folder_path) -> str | None:
|
||||
return os.path.join(folder_path, cover_files[0])
|
||||
|
||||
|
||||
def get_first_image_file_in_folder(folder_path) -> str | None:
|
||||
image_files = [
|
||||
f for f in os.listdir(folder_path) if f.endswith('.png') or f.endswith('.jpg')
|
||||
]
|
||||
if not image_files:
|
||||
return None
|
||||
|
||||
return os.path.join(folder_path, image_files[0])
|
||||
|
||||
|
||||
def create_valid_filename(input_string: str):
|
||||
valid_chars = f'-_() {os.sep}{os.sep}'
|
||||
sanitized_string = ''.join(
|
||||
@@ -67,3 +78,26 @@ def zipdir(path, ziph, archiveroot):
|
||||
archiveroot, os.path.relpath(actual_file_path, path),
|
||||
)
|
||||
ziph.write(actual_file_path, archive_file_path)
|
||||
|
||||
|
||||
def random_10_digit_int():
|
||||
return random.randint(10 ** 9, 10 ** 10 - 1)
|
||||
|
||||
|
||||
def convert_egg_to_ogg_and_get_length(egg_path: str):
|
||||
ogg_path = egg_path.replace('.egg', '.ogg')
|
||||
subprocess.run(
|
||||
['ffmpeg', '-loglevel', 'quiet', '-y', '-i', egg_path, ogg_path],
|
||||
check=True,
|
||||
)
|
||||
ffprobe_cmd = [
|
||||
'ffprobe',
|
||||
'-i', ogg_path,
|
||||
'-show_entries', 'format=duration',
|
||||
'-v', 'error',
|
||||
'-of', 'default=noprint_wrappers=1:nokey=1',
|
||||
]
|
||||
duration_str = subprocess.check_output(
|
||||
ffprobe_cmd, text=True,
|
||||
).strip()
|
||||
return os.path.basename(ogg_path), float(duration_str)
|
||||
|
||||
Reference in New Issue
Block a user