Initial commit
This commit is contained in:
216
main.py
Normal file
216
main.py
Normal file
@@ -0,0 +1,216 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import mutagen
|
||||
from mutagen.easyid3 import EasyID3
|
||||
from mutagen.id3 import ID3, APIC
|
||||
import ffmpeg
|
||||
import mimetypes
|
||||
|
||||
EasyID3.RegisterTextKey('comment', 'COMM')
|
||||
|
||||
|
||||
def parse_beatmap(content):
|
||||
section = None
|
||||
section_content = None
|
||||
pure_sections = ['events', 'timingpoints']
|
||||
beatmap = {}
|
||||
for line in content.split('\n'):
|
||||
if line.startswith('//'):
|
||||
continue
|
||||
if line.startswith('['):
|
||||
if section and section_content:
|
||||
beatmap[section] = section_content
|
||||
section = line[1:line.index(']')].lower()
|
||||
section_content = dict()
|
||||
if section in pure_sections:
|
||||
section_content = list()
|
||||
continue
|
||||
if section in pure_sections:
|
||||
section_content.append(line)
|
||||
elif section and ':' in line:
|
||||
key = line[:line.index(':')].lower()
|
||||
value = line[line.index(':') + 1:].strip()
|
||||
section_content[key] = value
|
||||
return beatmap
|
||||
|
||||
|
||||
def scan_beatmaps(root):
|
||||
beatmap_sets = {}
|
||||
for beatmap_path in Path(root).glob('**/*.osu'):
|
||||
file = open(str(beatmap_path), 'r', encoding='utf-8').read()
|
||||
beatmap = parse_beatmap(file)
|
||||
beatmap_set = beatmap['metadata'].get('beatmapsetid')
|
||||
if not beatmap['metadata'].get('beatmapsetid'):
|
||||
p = beatmap_path.parent
|
||||
beatmap_set = p.name.split(' ')[0]
|
||||
if not beatmap_set.isdigit():
|
||||
beatmap_set = 'Unranked'
|
||||
bg = None
|
||||
video = None
|
||||
for event in beatmap['events']:
|
||||
if event.startswith('0,0,'):
|
||||
bg = event.split(',')[2].strip('"').strip()
|
||||
if event.startswith('Video'):
|
||||
video = [x.strip('"') for x in event.split(',')[1:]]
|
||||
video = {'timing': video[0], 'filename': video[1]}
|
||||
beatmap['background'] = bg
|
||||
beatmap['video'] = video
|
||||
beatmap['path'] = str(beatmap_path)
|
||||
if not beatmap_sets.get(beatmap_set):
|
||||
beatmap_sets[beatmap_set] = list()
|
||||
beatmap_sets[beatmap_set].append(beatmap)
|
||||
return beatmap_sets
|
||||
|
||||
|
||||
def generalize_beatmap_sets(beatmap_sets):
|
||||
# Generalize to only relevant data
|
||||
generalized = {}
|
||||
unique_warn = []
|
||||
if beatmap_sets.get('Unranked'):
|
||||
beatmap_sets.pop('Unranked')
|
||||
for setid in beatmap_sets.keys():
|
||||
set_data = {}
|
||||
for map in beatmap_sets[setid]:
|
||||
data = map['metadata']
|
||||
data.pop('version')
|
||||
if data.get('beatmapid'):
|
||||
data.pop('beatmapid')
|
||||
data.pop('beatmapsetid')
|
||||
beatmap_dir = Path(map['path']).parent
|
||||
data['audio'] = None
|
||||
if map['general']['audiofilename']:
|
||||
data['audio'] = str(beatmap_dir.joinpath(map['general']['audiofilename']).absolute())
|
||||
data['video'] = map['video']
|
||||
if data['video']:
|
||||
data['video']['filename'] = str(beatmap_dir.joinpath(data['video']['filename']).absolute())
|
||||
if not os.path.exists(data['video']['filename']):
|
||||
if setid not in unique_warn:
|
||||
print(f'Video for {setid} is mentioned, but doesn\'t exist!')
|
||||
unique_warn.append(setid)
|
||||
data['video'] = None
|
||||
data['thumbnail'] = None
|
||||
if map['background']:
|
||||
data['thumbnail'] = str(beatmap_dir.joinpath(map['background']).absolute())
|
||||
for k, v in data.items():
|
||||
if not v:
|
||||
continue
|
||||
if not set_data.get(k) or all([k == 'thumbnail', v != '', v]):
|
||||
set_data[k] = v
|
||||
if set_data.get(k) != v:
|
||||
if k == 'tags' and len(v) > len(set_data.get(k)):
|
||||
set_data[k] = v
|
||||
# print(f"{map['path']}: Conflict of data with set ({k}) | {set_data.get(k)} != {v}")
|
||||
generalized[setid] = set_data
|
||||
return generalized
|
||||
|
||||
|
||||
def clean_and_allow_filename(dirty_filename, invalid='<>:"/\|?*'):
|
||||
fn = str(dirty_filename)
|
||||
for char in invalid:
|
||||
fn = fn.replace(char, '')
|
||||
return fn
|
||||
|
||||
|
||||
def generate_library(beatmap_sets, music=True, video=False, music_target=None, video_target=None):
|
||||
if music:
|
||||
if not music_target:
|
||||
music_target = f'{os.getcwd()}{os.path.sep}osu!MusicLibrary'
|
||||
try:
|
||||
os.mkdir(f"{music_target}")
|
||||
except:
|
||||
pass
|
||||
for setid, beatmap in beatmap_sets.items():
|
||||
try:
|
||||
map = dict(beatmap)
|
||||
dir_name = f"{map['title']} by {map['artist']} ({map['creator']})"
|
||||
dir_name = clean_and_allow_filename(dir_name)
|
||||
try:
|
||||
os.mkdir(f"{music_target}{os.path.sep}{dir_name}")
|
||||
except:
|
||||
pass
|
||||
if not map['audio'] or map['audio'] == 'virtual':
|
||||
continue
|
||||
ext = os.path.splitext(map['audio'])[1]
|
||||
fn = f"{map['artist']} - {map['title']}{ext}"
|
||||
fn = clean_and_allow_filename(fn)
|
||||
file_target = f"{music_target}{os.path.sep}{dir_name}{os.path.sep}{fn}"
|
||||
shutil.copy2(map['audio'], file_target)
|
||||
map['audio'] = file_target
|
||||
if map.get('thumbnail') and os.path.exists(map['thumbnail']):
|
||||
ext = os.path.splitext(map['thumbnail'])[1]
|
||||
file_target = f"{music_target}{os.path.sep}{dir_name}{os.path.sep}cover{ext}"
|
||||
shutil.copy2(map['thumbnail'], file_target)
|
||||
map['thumbnail'] = file_target
|
||||
audiofile = mutagen.File(map['audio'], easy=True)
|
||||
if audiofile:
|
||||
audiofile['artist'] = map['artist']
|
||||
audiofile['album'] = map['creator']
|
||||
audiofile['albumartist'] = map.get('artistunicode') if map.get('artistunicode') else map['artist']
|
||||
audiofile['title'] = map['title']
|
||||
if map.get('tags'):
|
||||
audiofile['comment'] = map['tags']
|
||||
audiofile['tracknumber'] = ['1', '1']
|
||||
audiofile.save()
|
||||
if map.get('thumbnail') and not map['audio'].endswith('.ogg'):
|
||||
audio = mutagen.File(map['audio'], easy=False)
|
||||
if 'audio/vorbis' in audio.mime:
|
||||
continue
|
||||
with open(map.get('thumbnail'), 'rb') as albumart:
|
||||
audio['APIC'] = APIC(
|
||||
encoding=3,
|
||||
mime=mimetypes.guess_type(map.get('thumbnail')),
|
||||
type=3, desc='osu! Beatmap Thumbnail',
|
||||
data=albumart.read()
|
||||
)
|
||||
audio.save()
|
||||
except Exception as error:
|
||||
print(f'[Music] Failure while processing {setid} | {type(error)} | {str(error)}')
|
||||
if video:
|
||||
if not video_target:
|
||||
video_target = f'{os.getcwd()}{os.path.sep}osu!VideoLibrary'
|
||||
try:
|
||||
os.mkdir(f"{video_target}")
|
||||
except:
|
||||
pass
|
||||
for setid, beatmap in beatmap_sets.items():
|
||||
try:
|
||||
if not beatmap.get('video') or beatmap.get('audio').endswith('virtual'):
|
||||
continue
|
||||
map = dict(beatmap)
|
||||
fn = f"{map['artist']} - {map['title']} ({map['creator']}).mp4"
|
||||
output_fp = f"{video_target}{os.path.sep}{clean_and_allow_filename(fn)}"
|
||||
audio_in = ffmpeg.input(map['audio'])['a']
|
||||
kw = {}
|
||||
if map['video']['timing'] != '0':
|
||||
kw['itsoffset'] = float(map['video']['timing']) / 1000
|
||||
video_in = ffmpeg.input(map['video']['filename'], **kw)
|
||||
streams = ffmpeg.probe(map['video']['filename'])['streams']
|
||||
ow = False
|
||||
for stream in streams:
|
||||
if stream.get('codec_name') in ['h264', 'avc1', 'mpeg4']:
|
||||
video_in = video_in[str(stream['index'])]
|
||||
ow = True
|
||||
if not ow:
|
||||
for stream in streams:
|
||||
if stream.get('codec_name') not in ['h264', 'avc1', 'mpeg4']:
|
||||
output_fp = output_fp[:-len('mp4')] + 'mkv'
|
||||
out = ffmpeg.output(audio_in, video_in, output_fp, vcodec='copy', acodec='copy', fflags='+genpts')
|
||||
print(' '.join(out.compile()))
|
||||
out.run(overwrite_output=True)
|
||||
except Exception as error:
|
||||
print(f'[Video] Failure while processing {setid} | {type(error)} | {str(error)}')
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
root = os.path.abspath(os.environ['LOCALAPPDATA'] + '\\osu!\\Songs\\')
|
||||
print('Scanning Beatmaps')
|
||||
beatmap_sets = scan_beatmaps(root)
|
||||
print('Generalizing beatmap data')
|
||||
beatmaps = generalize_beatmap_sets(beatmap_sets)
|
||||
print('Generating Music & Video Libraries')
|
||||
generate_library(beatmaps,
|
||||
music=input('Music Library? (y/n) ').lower().startswith('y'),
|
||||
video=input('Video Library? (y/n) ').lower().startswith('y'))
|
||||
print('Done, thanks for usage!')
|
||||
Reference in New Issue
Block a user