Converting MP3 albums into MP4 videos for YouTube
$begingroup$
This is a quick script that helps me to convert MP3 files from my CDs into videos I can upload on YouTube. It is intended for personal use, so no input error checks. I am creating a silent video using OpenCV, combining MP3s using pydub, then put audio and video together using ffmpeg.
I am happy with the resulting videos. The code looks a bit naive, but introducing more complex structures is probably an overkill given the simplicity of the task.
This code is also on GitHub.
import os, codecs, datetime, glob
import cv2, pydub, PIL # these packages need to be installed
from PIL import ImageFont, ImageDraw
# get filenames with given extensions from a given directory and all directories inside it
def get_filenames_with_extensions_recursively(directory_name, extensions):
result =
for extension in extensions:
path_pattern = os.path.join(directory_name, '**', '*.' + extension)
result += glob.glob(path_pattern, recursive=True)
return result
# Score function for default audio sorting: directory containing the file,
# then the number of the track, then the name of the file
def default_func_sort_audio_files(audio_name):
try:
audio_mediainfo = pydub.utils.mediainfo(audio_name).get('TAG', None)
track_str = audio_mediainfo['track']
track_nb_str = track_str.split('/')
track_nb = int(track_nb_str[0])
except:
track_nb = -1
return (os.path.dirname(audio_name), track_nb, os.path.basename(audio_name))
# Resize image, add subtitles and save it.
# Returns the filename of the resulting image (including the path)
def add_subtitles(image_filename,
temp_folder,
width,
height,
subtitles,
font,
sub_colour,
sub_bg_colour,
sub_indent_x):
# make a blank completely transparent image for the rectangle
with PIL.Image.new('RGBA', (width, height), (0, 0, 0, 0)) as img2:
# get a drawing context for it
draw = PIL.ImageDraw.Draw(img2)
# create the background coloured box
max_length_subtitles = 0
for subtitle in subtitles:
sub_size = font.getsize(subtitle)
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
sub_bg_right = max_length_subtitles + 2 * sub_indent_x
if sub_bg_right > width:
sub_bg_right = width
sub_bg_top = height - len(subtitles) * 2 * font.size - sub_indent_x
draw.rectangle(((0, sub_bg_top), (sub_bg_right, height)), fill = sub_bg_colour)
# add subtitles
sub_indent_y = height
for subtitle in reversed(subtitles):
sub_indent_y -= 2 * font.size
draw.text((sub_indent_x, sub_indent_y), subtitle, sub_colour, font = font)
with PIL.Image.open(image_filename) as img:
img = img.resize((width, height), PIL.Image.ANTIALIAS)
img = img.convert("RGBA")
# composite the two images together and save
temp_image_filename
= os.path.join(temp_folder,
os.path.basename(image_filename) + '_with_subs.png')
with PIL.Image.alpha_composite(img, img2) as img_full:
img_full.save(temp_image_filename)
return temp_image_filename
# The main function. It creates the video with all audio files of a given directory
# All images with given extensions from the same directory are fetched.
# While an audio track is being played, one image, with the subtitles, is shown.
# Images are shown in alphabetic order.
# Audio tracks are sorted using 'func_sort_audio_files'
# Descriptions and subtitles are obtained using 'func_get_audio_description_subtitles'
# Outputs: a compilation video
# a text file that contains the description of the tracks that constitute the video
def make_video( directory_name,
func_get_audio_description_subtitles,
video_title = None,
artist_override = None,
func_sort_audio_files = default_func_sort_audio_files,
width = 1280,
height = 720,
sub_font_size = 32,
sub_font_name = "/System/Library/Fonts/SFNSText.ttf",
sub_encoding = "unic",
sub_colour = (255, 255, 255),
# 4th number in sub_bg_colour is for the degree of transparency, 0 - 255 range
sub_bg_colour = (0, 0, 0, 128),
sub_indent_x = 10,
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
dry_run = False):
start_time = datetime.datetime.now()
# prepare the temp directory
temp_folder = os.path.join(directory_name, 'temp')
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
extensions_to_remove = image_extensions + audio_extensions
if not dry_run:
extensions_to_remove += ['mp4']
filenames_to_remove = get_filenames_with_extensions_recursively(temp_folder, extensions_to_remove)
for fn in filenames_to_remove:
os.remove(fn)
# get the filenames and sort them
images_filenames = get_filenames_with_extensions_recursively(directory_name, image_extensions)
images_filenames.sort()
audio_filenames = get_filenames_with_extensions_recursively(directory_name, audio_extensions)
audio_filenames.sort(key = lambda af: func_sort_audio_files(af))
# initiate variables
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
font = PIL.ImageFont.truetype(sub_font_name, sub_font_size, encoding = sub_encoding)
silent_video_name = os.path.join(temp_folder, os.path.basename(directory_name) + '_silent.mp4')
video = cv2.VideoWriter(silent_video_name, fourcc, 1.0, (width, height))
descriptions = description_intro
audio = pydub.AudioSegment.silent(duration = 0)
counter_audio = 0
counter_seconds = 0
for audio_name in audio_filenames:
audio_mediainfo = {}
try:
audio_mediainfo = pydub.utils.mediainfo(audio_name).get('TAG', None)
except:
pass
if not ('title' in audio_mediainfo):
track_name = os.path.basename(audio_name)
# remove the extension
track_name = track_name[:track_name.rfind('.')]
audio_mediainfo['title'] = track_name
if not ('artist' in audio_mediainfo):
audio_mediainfo['artist'] = ''
if (artist_override != None):
audio_mediainfo['artist'] = artist_override
counter_audio += 1
description, subtitles = func_get_audio_description_subtitles(counter_audio, audio_mediainfo)
descriptions += [str(datetime.timedelta(seconds=counter_seconds)) + " " + description]
if not dry_run:
image_filename = images_filenames[counter_audio % len(images_filenames)]
temp_image_filename = add_subtitles( image_filename,
temp_folder,
width,
height,
subtitles,
font,
sub_colour,
sub_bg_colour,
sub_indent_x)
img2 = cv2.imread(temp_image_filename)
else:
img2 = None
audio_piece = pydub.AudioSegment.from_mp3(audio_name)
limit_audio_length_so_far = counter_seconds + audio_piece.duration_seconds
while (counter_seconds <= limit_audio_length_so_far):
if not dry_run:
# add the image to the video using PIL (adding by 1sec-long frames)
video.write(img2)
counter_seconds += 1
if not dry_run:
audio += audio_piece
# match the duration of audio and video so far
audio += pydub.AudioSegment.silent(duration = (counter_seconds * 1000.0 - len(audio)))
# Finalize the silent video
cv2.destroyAllWindows()
video.release()
# Define the filenames
if video_title == None:
video_title = os.path.basename(directory_name)
descriptions_file_path = os.path.join(temp_folder, video_title + '.txt')
compilation_audio_name = os.path.join(temp_folder, video_title + '.mp3')
video_name = os.path.join(temp_folder, video_title + '.mp4')
ffmpeg_output_path = os.path.join(temp_folder, video_title + '_ffmpeg.txt')
if not dry_run:
# dump the long mp3
audio.export(compilation_audio_name, format = "mp3")
# combine audio and silent video into the final video
ffmpeg_cmd = 'ffmpeg -i "' + silent_video_name + '" -i "' + compilation_audio_name
+ '" -shortest -c:v copy -c:a aac -b:a 256k "' + video_name + '"'
+ ' > "'+ ffmpeg_output_path + '" 2>&1'
os.system(ffmpeg_cmd)
# Finalize and output the descriptions
descriptions_len = 0
for d_line in descriptions:
descriptions_len += len(d_line)
separator = "*" * 80
descriptions = [separator,
"Directory: " + directory_name,
separator]
+ descriptions
+ [separator,
"The length of the video is " + str(counter_seconds / 60.0) + " minute(s)",
"It should be under 202-205min (this is a pydub limitation)",
separator,
"Description is " + str(descriptions_len) + " characters long",
"It should be under 4500-5000 characters long (this is a youtube limitation)",
separator,
"Started " + str(start_time) + ", completed " + str(datetime.datetime.now()),
separator]
for d_line in descriptions:
print (d_line)
with codecs.open(descriptions_file_path, 'w', encoding = file_encoding) as the_file:
the_file.writelines(d_line + "n" for d_line in (descriptions))
def get_audio_description_subtitles_simple(counter_audio, audio_mediainfo):
title = audio_mediainfo['title'].strip().replace('\', '')
track_name = 'Track ' + str(counter_audio) + ": " + title
artist_name = audio_mediainfo['artist'].strip()
desc = track_name + " by " + artist_name
return desc, [track_name, artist_name]
def dry_run_get_audio_description_subtitles_dry_run(counter_audio, audio_mediainfo):
print(audio_mediainfo)
return "", ""
if __name__ == '__main__':
make_video( directory_name = os.path.expanduser('~/Music/LouisXIII copy'),
#artist_override = 'Dalida',
func_get_audio_description_subtitles = get_audio_description_subtitles_simple,
description_intro = ['Intended for personal use. I own the CDs', ''],
dry_run = True)
python python-3.x opencv video
$endgroup$
add a comment |
$begingroup$
This is a quick script that helps me to convert MP3 files from my CDs into videos I can upload on YouTube. It is intended for personal use, so no input error checks. I am creating a silent video using OpenCV, combining MP3s using pydub, then put audio and video together using ffmpeg.
I am happy with the resulting videos. The code looks a bit naive, but introducing more complex structures is probably an overkill given the simplicity of the task.
This code is also on GitHub.
import os, codecs, datetime, glob
import cv2, pydub, PIL # these packages need to be installed
from PIL import ImageFont, ImageDraw
# get filenames with given extensions from a given directory and all directories inside it
def get_filenames_with_extensions_recursively(directory_name, extensions):
result =
for extension in extensions:
path_pattern = os.path.join(directory_name, '**', '*.' + extension)
result += glob.glob(path_pattern, recursive=True)
return result
# Score function for default audio sorting: directory containing the file,
# then the number of the track, then the name of the file
def default_func_sort_audio_files(audio_name):
try:
audio_mediainfo = pydub.utils.mediainfo(audio_name).get('TAG', None)
track_str = audio_mediainfo['track']
track_nb_str = track_str.split('/')
track_nb = int(track_nb_str[0])
except:
track_nb = -1
return (os.path.dirname(audio_name), track_nb, os.path.basename(audio_name))
# Resize image, add subtitles and save it.
# Returns the filename of the resulting image (including the path)
def add_subtitles(image_filename,
temp_folder,
width,
height,
subtitles,
font,
sub_colour,
sub_bg_colour,
sub_indent_x):
# make a blank completely transparent image for the rectangle
with PIL.Image.new('RGBA', (width, height), (0, 0, 0, 0)) as img2:
# get a drawing context for it
draw = PIL.ImageDraw.Draw(img2)
# create the background coloured box
max_length_subtitles = 0
for subtitle in subtitles:
sub_size = font.getsize(subtitle)
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
sub_bg_right = max_length_subtitles + 2 * sub_indent_x
if sub_bg_right > width:
sub_bg_right = width
sub_bg_top = height - len(subtitles) * 2 * font.size - sub_indent_x
draw.rectangle(((0, sub_bg_top), (sub_bg_right, height)), fill = sub_bg_colour)
# add subtitles
sub_indent_y = height
for subtitle in reversed(subtitles):
sub_indent_y -= 2 * font.size
draw.text((sub_indent_x, sub_indent_y), subtitle, sub_colour, font = font)
with PIL.Image.open(image_filename) as img:
img = img.resize((width, height), PIL.Image.ANTIALIAS)
img = img.convert("RGBA")
# composite the two images together and save
temp_image_filename
= os.path.join(temp_folder,
os.path.basename(image_filename) + '_with_subs.png')
with PIL.Image.alpha_composite(img, img2) as img_full:
img_full.save(temp_image_filename)
return temp_image_filename
# The main function. It creates the video with all audio files of a given directory
# All images with given extensions from the same directory are fetched.
# While an audio track is being played, one image, with the subtitles, is shown.
# Images are shown in alphabetic order.
# Audio tracks are sorted using 'func_sort_audio_files'
# Descriptions and subtitles are obtained using 'func_get_audio_description_subtitles'
# Outputs: a compilation video
# a text file that contains the description of the tracks that constitute the video
def make_video( directory_name,
func_get_audio_description_subtitles,
video_title = None,
artist_override = None,
func_sort_audio_files = default_func_sort_audio_files,
width = 1280,
height = 720,
sub_font_size = 32,
sub_font_name = "/System/Library/Fonts/SFNSText.ttf",
sub_encoding = "unic",
sub_colour = (255, 255, 255),
# 4th number in sub_bg_colour is for the degree of transparency, 0 - 255 range
sub_bg_colour = (0, 0, 0, 128),
sub_indent_x = 10,
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
dry_run = False):
start_time = datetime.datetime.now()
# prepare the temp directory
temp_folder = os.path.join(directory_name, 'temp')
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
extensions_to_remove = image_extensions + audio_extensions
if not dry_run:
extensions_to_remove += ['mp4']
filenames_to_remove = get_filenames_with_extensions_recursively(temp_folder, extensions_to_remove)
for fn in filenames_to_remove:
os.remove(fn)
# get the filenames and sort them
images_filenames = get_filenames_with_extensions_recursively(directory_name, image_extensions)
images_filenames.sort()
audio_filenames = get_filenames_with_extensions_recursively(directory_name, audio_extensions)
audio_filenames.sort(key = lambda af: func_sort_audio_files(af))
# initiate variables
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
font = PIL.ImageFont.truetype(sub_font_name, sub_font_size, encoding = sub_encoding)
silent_video_name = os.path.join(temp_folder, os.path.basename(directory_name) + '_silent.mp4')
video = cv2.VideoWriter(silent_video_name, fourcc, 1.0, (width, height))
descriptions = description_intro
audio = pydub.AudioSegment.silent(duration = 0)
counter_audio = 0
counter_seconds = 0
for audio_name in audio_filenames:
audio_mediainfo = {}
try:
audio_mediainfo = pydub.utils.mediainfo(audio_name).get('TAG', None)
except:
pass
if not ('title' in audio_mediainfo):
track_name = os.path.basename(audio_name)
# remove the extension
track_name = track_name[:track_name.rfind('.')]
audio_mediainfo['title'] = track_name
if not ('artist' in audio_mediainfo):
audio_mediainfo['artist'] = ''
if (artist_override != None):
audio_mediainfo['artist'] = artist_override
counter_audio += 1
description, subtitles = func_get_audio_description_subtitles(counter_audio, audio_mediainfo)
descriptions += [str(datetime.timedelta(seconds=counter_seconds)) + " " + description]
if not dry_run:
image_filename = images_filenames[counter_audio % len(images_filenames)]
temp_image_filename = add_subtitles( image_filename,
temp_folder,
width,
height,
subtitles,
font,
sub_colour,
sub_bg_colour,
sub_indent_x)
img2 = cv2.imread(temp_image_filename)
else:
img2 = None
audio_piece = pydub.AudioSegment.from_mp3(audio_name)
limit_audio_length_so_far = counter_seconds + audio_piece.duration_seconds
while (counter_seconds <= limit_audio_length_so_far):
if not dry_run:
# add the image to the video using PIL (adding by 1sec-long frames)
video.write(img2)
counter_seconds += 1
if not dry_run:
audio += audio_piece
# match the duration of audio and video so far
audio += pydub.AudioSegment.silent(duration = (counter_seconds * 1000.0 - len(audio)))
# Finalize the silent video
cv2.destroyAllWindows()
video.release()
# Define the filenames
if video_title == None:
video_title = os.path.basename(directory_name)
descriptions_file_path = os.path.join(temp_folder, video_title + '.txt')
compilation_audio_name = os.path.join(temp_folder, video_title + '.mp3')
video_name = os.path.join(temp_folder, video_title + '.mp4')
ffmpeg_output_path = os.path.join(temp_folder, video_title + '_ffmpeg.txt')
if not dry_run:
# dump the long mp3
audio.export(compilation_audio_name, format = "mp3")
# combine audio and silent video into the final video
ffmpeg_cmd = 'ffmpeg -i "' + silent_video_name + '" -i "' + compilation_audio_name
+ '" -shortest -c:v copy -c:a aac -b:a 256k "' + video_name + '"'
+ ' > "'+ ffmpeg_output_path + '" 2>&1'
os.system(ffmpeg_cmd)
# Finalize and output the descriptions
descriptions_len = 0
for d_line in descriptions:
descriptions_len += len(d_line)
separator = "*" * 80
descriptions = [separator,
"Directory: " + directory_name,
separator]
+ descriptions
+ [separator,
"The length of the video is " + str(counter_seconds / 60.0) + " minute(s)",
"It should be under 202-205min (this is a pydub limitation)",
separator,
"Description is " + str(descriptions_len) + " characters long",
"It should be under 4500-5000 characters long (this is a youtube limitation)",
separator,
"Started " + str(start_time) + ", completed " + str(datetime.datetime.now()),
separator]
for d_line in descriptions:
print (d_line)
with codecs.open(descriptions_file_path, 'w', encoding = file_encoding) as the_file:
the_file.writelines(d_line + "n" for d_line in (descriptions))
def get_audio_description_subtitles_simple(counter_audio, audio_mediainfo):
title = audio_mediainfo['title'].strip().replace('\', '')
track_name = 'Track ' + str(counter_audio) + ": " + title
artist_name = audio_mediainfo['artist'].strip()
desc = track_name + " by " + artist_name
return desc, [track_name, artist_name]
def dry_run_get_audio_description_subtitles_dry_run(counter_audio, audio_mediainfo):
print(audio_mediainfo)
return "", ""
if __name__ == '__main__':
make_video( directory_name = os.path.expanduser('~/Music/LouisXIII copy'),
#artist_override = 'Dalida',
func_get_audio_description_subtitles = get_audio_description_subtitles_simple,
description_intro = ['Intended for personal use. I own the CDs', ''],
dry_run = True)
python python-3.x opencv video
$endgroup$
add a comment |
$begingroup$
This is a quick script that helps me to convert MP3 files from my CDs into videos I can upload on YouTube. It is intended for personal use, so no input error checks. I am creating a silent video using OpenCV, combining MP3s using pydub, then put audio and video together using ffmpeg.
I am happy with the resulting videos. The code looks a bit naive, but introducing more complex structures is probably an overkill given the simplicity of the task.
This code is also on GitHub.
import os, codecs, datetime, glob
import cv2, pydub, PIL # these packages need to be installed
from PIL import ImageFont, ImageDraw
# get filenames with given extensions from a given directory and all directories inside it
def get_filenames_with_extensions_recursively(directory_name, extensions):
result =
for extension in extensions:
path_pattern = os.path.join(directory_name, '**', '*.' + extension)
result += glob.glob(path_pattern, recursive=True)
return result
# Score function for default audio sorting: directory containing the file,
# then the number of the track, then the name of the file
def default_func_sort_audio_files(audio_name):
try:
audio_mediainfo = pydub.utils.mediainfo(audio_name).get('TAG', None)
track_str = audio_mediainfo['track']
track_nb_str = track_str.split('/')
track_nb = int(track_nb_str[0])
except:
track_nb = -1
return (os.path.dirname(audio_name), track_nb, os.path.basename(audio_name))
# Resize image, add subtitles and save it.
# Returns the filename of the resulting image (including the path)
def add_subtitles(image_filename,
temp_folder,
width,
height,
subtitles,
font,
sub_colour,
sub_bg_colour,
sub_indent_x):
# make a blank completely transparent image for the rectangle
with PIL.Image.new('RGBA', (width, height), (0, 0, 0, 0)) as img2:
# get a drawing context for it
draw = PIL.ImageDraw.Draw(img2)
# create the background coloured box
max_length_subtitles = 0
for subtitle in subtitles:
sub_size = font.getsize(subtitle)
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
sub_bg_right = max_length_subtitles + 2 * sub_indent_x
if sub_bg_right > width:
sub_bg_right = width
sub_bg_top = height - len(subtitles) * 2 * font.size - sub_indent_x
draw.rectangle(((0, sub_bg_top), (sub_bg_right, height)), fill = sub_bg_colour)
# add subtitles
sub_indent_y = height
for subtitle in reversed(subtitles):
sub_indent_y -= 2 * font.size
draw.text((sub_indent_x, sub_indent_y), subtitle, sub_colour, font = font)
with PIL.Image.open(image_filename) as img:
img = img.resize((width, height), PIL.Image.ANTIALIAS)
img = img.convert("RGBA")
# composite the two images together and save
temp_image_filename
= os.path.join(temp_folder,
os.path.basename(image_filename) + '_with_subs.png')
with PIL.Image.alpha_composite(img, img2) as img_full:
img_full.save(temp_image_filename)
return temp_image_filename
# The main function. It creates the video with all audio files of a given directory
# All images with given extensions from the same directory are fetched.
# While an audio track is being played, one image, with the subtitles, is shown.
# Images are shown in alphabetic order.
# Audio tracks are sorted using 'func_sort_audio_files'
# Descriptions and subtitles are obtained using 'func_get_audio_description_subtitles'
# Outputs: a compilation video
# a text file that contains the description of the tracks that constitute the video
def make_video( directory_name,
func_get_audio_description_subtitles,
video_title = None,
artist_override = None,
func_sort_audio_files = default_func_sort_audio_files,
width = 1280,
height = 720,
sub_font_size = 32,
sub_font_name = "/System/Library/Fonts/SFNSText.ttf",
sub_encoding = "unic",
sub_colour = (255, 255, 255),
# 4th number in sub_bg_colour is for the degree of transparency, 0 - 255 range
sub_bg_colour = (0, 0, 0, 128),
sub_indent_x = 10,
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
dry_run = False):
start_time = datetime.datetime.now()
# prepare the temp directory
temp_folder = os.path.join(directory_name, 'temp')
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
extensions_to_remove = image_extensions + audio_extensions
if not dry_run:
extensions_to_remove += ['mp4']
filenames_to_remove = get_filenames_with_extensions_recursively(temp_folder, extensions_to_remove)
for fn in filenames_to_remove:
os.remove(fn)
# get the filenames and sort them
images_filenames = get_filenames_with_extensions_recursively(directory_name, image_extensions)
images_filenames.sort()
audio_filenames = get_filenames_with_extensions_recursively(directory_name, audio_extensions)
audio_filenames.sort(key = lambda af: func_sort_audio_files(af))
# initiate variables
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
font = PIL.ImageFont.truetype(sub_font_name, sub_font_size, encoding = sub_encoding)
silent_video_name = os.path.join(temp_folder, os.path.basename(directory_name) + '_silent.mp4')
video = cv2.VideoWriter(silent_video_name, fourcc, 1.0, (width, height))
descriptions = description_intro
audio = pydub.AudioSegment.silent(duration = 0)
counter_audio = 0
counter_seconds = 0
for audio_name in audio_filenames:
audio_mediainfo = {}
try:
audio_mediainfo = pydub.utils.mediainfo(audio_name).get('TAG', None)
except:
pass
if not ('title' in audio_mediainfo):
track_name = os.path.basename(audio_name)
# remove the extension
track_name = track_name[:track_name.rfind('.')]
audio_mediainfo['title'] = track_name
if not ('artist' in audio_mediainfo):
audio_mediainfo['artist'] = ''
if (artist_override != None):
audio_mediainfo['artist'] = artist_override
counter_audio += 1
description, subtitles = func_get_audio_description_subtitles(counter_audio, audio_mediainfo)
descriptions += [str(datetime.timedelta(seconds=counter_seconds)) + " " + description]
if not dry_run:
image_filename = images_filenames[counter_audio % len(images_filenames)]
temp_image_filename = add_subtitles( image_filename,
temp_folder,
width,
height,
subtitles,
font,
sub_colour,
sub_bg_colour,
sub_indent_x)
img2 = cv2.imread(temp_image_filename)
else:
img2 = None
audio_piece = pydub.AudioSegment.from_mp3(audio_name)
limit_audio_length_so_far = counter_seconds + audio_piece.duration_seconds
while (counter_seconds <= limit_audio_length_so_far):
if not dry_run:
# add the image to the video using PIL (adding by 1sec-long frames)
video.write(img2)
counter_seconds += 1
if not dry_run:
audio += audio_piece
# match the duration of audio and video so far
audio += pydub.AudioSegment.silent(duration = (counter_seconds * 1000.0 - len(audio)))
# Finalize the silent video
cv2.destroyAllWindows()
video.release()
# Define the filenames
if video_title == None:
video_title = os.path.basename(directory_name)
descriptions_file_path = os.path.join(temp_folder, video_title + '.txt')
compilation_audio_name = os.path.join(temp_folder, video_title + '.mp3')
video_name = os.path.join(temp_folder, video_title + '.mp4')
ffmpeg_output_path = os.path.join(temp_folder, video_title + '_ffmpeg.txt')
if not dry_run:
# dump the long mp3
audio.export(compilation_audio_name, format = "mp3")
# combine audio and silent video into the final video
ffmpeg_cmd = 'ffmpeg -i "' + silent_video_name + '" -i "' + compilation_audio_name
+ '" -shortest -c:v copy -c:a aac -b:a 256k "' + video_name + '"'
+ ' > "'+ ffmpeg_output_path + '" 2>&1'
os.system(ffmpeg_cmd)
# Finalize and output the descriptions
descriptions_len = 0
for d_line in descriptions:
descriptions_len += len(d_line)
separator = "*" * 80
descriptions = [separator,
"Directory: " + directory_name,
separator]
+ descriptions
+ [separator,
"The length of the video is " + str(counter_seconds / 60.0) + " minute(s)",
"It should be under 202-205min (this is a pydub limitation)",
separator,
"Description is " + str(descriptions_len) + " characters long",
"It should be under 4500-5000 characters long (this is a youtube limitation)",
separator,
"Started " + str(start_time) + ", completed " + str(datetime.datetime.now()),
separator]
for d_line in descriptions:
print (d_line)
with codecs.open(descriptions_file_path, 'w', encoding = file_encoding) as the_file:
the_file.writelines(d_line + "n" for d_line in (descriptions))
def get_audio_description_subtitles_simple(counter_audio, audio_mediainfo):
title = audio_mediainfo['title'].strip().replace('\', '')
track_name = 'Track ' + str(counter_audio) + ": " + title
artist_name = audio_mediainfo['artist'].strip()
desc = track_name + " by " + artist_name
return desc, [track_name, artist_name]
def dry_run_get_audio_description_subtitles_dry_run(counter_audio, audio_mediainfo):
print(audio_mediainfo)
return "", ""
if __name__ == '__main__':
make_video( directory_name = os.path.expanduser('~/Music/LouisXIII copy'),
#artist_override = 'Dalida',
func_get_audio_description_subtitles = get_audio_description_subtitles_simple,
description_intro = ['Intended for personal use. I own the CDs', ''],
dry_run = True)
python python-3.x opencv video
$endgroup$
This is a quick script that helps me to convert MP3 files from my CDs into videos I can upload on YouTube. It is intended for personal use, so no input error checks. I am creating a silent video using OpenCV, combining MP3s using pydub, then put audio and video together using ffmpeg.
I am happy with the resulting videos. The code looks a bit naive, but introducing more complex structures is probably an overkill given the simplicity of the task.
This code is also on GitHub.
import os, codecs, datetime, glob
import cv2, pydub, PIL # these packages need to be installed
from PIL import ImageFont, ImageDraw
# get filenames with given extensions from a given directory and all directories inside it
def get_filenames_with_extensions_recursively(directory_name, extensions):
result =
for extension in extensions:
path_pattern = os.path.join(directory_name, '**', '*.' + extension)
result += glob.glob(path_pattern, recursive=True)
return result
# Score function for default audio sorting: directory containing the file,
# then the number of the track, then the name of the file
def default_func_sort_audio_files(audio_name):
try:
audio_mediainfo = pydub.utils.mediainfo(audio_name).get('TAG', None)
track_str = audio_mediainfo['track']
track_nb_str = track_str.split('/')
track_nb = int(track_nb_str[0])
except:
track_nb = -1
return (os.path.dirname(audio_name), track_nb, os.path.basename(audio_name))
# Resize image, add subtitles and save it.
# Returns the filename of the resulting image (including the path)
def add_subtitles(image_filename,
temp_folder,
width,
height,
subtitles,
font,
sub_colour,
sub_bg_colour,
sub_indent_x):
# make a blank completely transparent image for the rectangle
with PIL.Image.new('RGBA', (width, height), (0, 0, 0, 0)) as img2:
# get a drawing context for it
draw = PIL.ImageDraw.Draw(img2)
# create the background coloured box
max_length_subtitles = 0
for subtitle in subtitles:
sub_size = font.getsize(subtitle)
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
sub_bg_right = max_length_subtitles + 2 * sub_indent_x
if sub_bg_right > width:
sub_bg_right = width
sub_bg_top = height - len(subtitles) * 2 * font.size - sub_indent_x
draw.rectangle(((0, sub_bg_top), (sub_bg_right, height)), fill = sub_bg_colour)
# add subtitles
sub_indent_y = height
for subtitle in reversed(subtitles):
sub_indent_y -= 2 * font.size
draw.text((sub_indent_x, sub_indent_y), subtitle, sub_colour, font = font)
with PIL.Image.open(image_filename) as img:
img = img.resize((width, height), PIL.Image.ANTIALIAS)
img = img.convert("RGBA")
# composite the two images together and save
temp_image_filename
= os.path.join(temp_folder,
os.path.basename(image_filename) + '_with_subs.png')
with PIL.Image.alpha_composite(img, img2) as img_full:
img_full.save(temp_image_filename)
return temp_image_filename
# The main function. It creates the video with all audio files of a given directory
# All images with given extensions from the same directory are fetched.
# While an audio track is being played, one image, with the subtitles, is shown.
# Images are shown in alphabetic order.
# Audio tracks are sorted using 'func_sort_audio_files'
# Descriptions and subtitles are obtained using 'func_get_audio_description_subtitles'
# Outputs: a compilation video
# a text file that contains the description of the tracks that constitute the video
def make_video( directory_name,
func_get_audio_description_subtitles,
video_title = None,
artist_override = None,
func_sort_audio_files = default_func_sort_audio_files,
width = 1280,
height = 720,
sub_font_size = 32,
sub_font_name = "/System/Library/Fonts/SFNSText.ttf",
sub_encoding = "unic",
sub_colour = (255, 255, 255),
# 4th number in sub_bg_colour is for the degree of transparency, 0 - 255 range
sub_bg_colour = (0, 0, 0, 128),
sub_indent_x = 10,
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
dry_run = False):
start_time = datetime.datetime.now()
# prepare the temp directory
temp_folder = os.path.join(directory_name, 'temp')
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
extensions_to_remove = image_extensions + audio_extensions
if not dry_run:
extensions_to_remove += ['mp4']
filenames_to_remove = get_filenames_with_extensions_recursively(temp_folder, extensions_to_remove)
for fn in filenames_to_remove:
os.remove(fn)
# get the filenames and sort them
images_filenames = get_filenames_with_extensions_recursively(directory_name, image_extensions)
images_filenames.sort()
audio_filenames = get_filenames_with_extensions_recursively(directory_name, audio_extensions)
audio_filenames.sort(key = lambda af: func_sort_audio_files(af))
# initiate variables
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
font = PIL.ImageFont.truetype(sub_font_name, sub_font_size, encoding = sub_encoding)
silent_video_name = os.path.join(temp_folder, os.path.basename(directory_name) + '_silent.mp4')
video = cv2.VideoWriter(silent_video_name, fourcc, 1.0, (width, height))
descriptions = description_intro
audio = pydub.AudioSegment.silent(duration = 0)
counter_audio = 0
counter_seconds = 0
for audio_name in audio_filenames:
audio_mediainfo = {}
try:
audio_mediainfo = pydub.utils.mediainfo(audio_name).get('TAG', None)
except:
pass
if not ('title' in audio_mediainfo):
track_name = os.path.basename(audio_name)
# remove the extension
track_name = track_name[:track_name.rfind('.')]
audio_mediainfo['title'] = track_name
if not ('artist' in audio_mediainfo):
audio_mediainfo['artist'] = ''
if (artist_override != None):
audio_mediainfo['artist'] = artist_override
counter_audio += 1
description, subtitles = func_get_audio_description_subtitles(counter_audio, audio_mediainfo)
descriptions += [str(datetime.timedelta(seconds=counter_seconds)) + " " + description]
if not dry_run:
image_filename = images_filenames[counter_audio % len(images_filenames)]
temp_image_filename = add_subtitles( image_filename,
temp_folder,
width,
height,
subtitles,
font,
sub_colour,
sub_bg_colour,
sub_indent_x)
img2 = cv2.imread(temp_image_filename)
else:
img2 = None
audio_piece = pydub.AudioSegment.from_mp3(audio_name)
limit_audio_length_so_far = counter_seconds + audio_piece.duration_seconds
while (counter_seconds <= limit_audio_length_so_far):
if not dry_run:
# add the image to the video using PIL (adding by 1sec-long frames)
video.write(img2)
counter_seconds += 1
if not dry_run:
audio += audio_piece
# match the duration of audio and video so far
audio += pydub.AudioSegment.silent(duration = (counter_seconds * 1000.0 - len(audio)))
# Finalize the silent video
cv2.destroyAllWindows()
video.release()
# Define the filenames
if video_title == None:
video_title = os.path.basename(directory_name)
descriptions_file_path = os.path.join(temp_folder, video_title + '.txt')
compilation_audio_name = os.path.join(temp_folder, video_title + '.mp3')
video_name = os.path.join(temp_folder, video_title + '.mp4')
ffmpeg_output_path = os.path.join(temp_folder, video_title + '_ffmpeg.txt')
if not dry_run:
# dump the long mp3
audio.export(compilation_audio_name, format = "mp3")
# combine audio and silent video into the final video
ffmpeg_cmd = 'ffmpeg -i "' + silent_video_name + '" -i "' + compilation_audio_name
+ '" -shortest -c:v copy -c:a aac -b:a 256k "' + video_name + '"'
+ ' > "'+ ffmpeg_output_path + '" 2>&1'
os.system(ffmpeg_cmd)
# Finalize and output the descriptions
descriptions_len = 0
for d_line in descriptions:
descriptions_len += len(d_line)
separator = "*" * 80
descriptions = [separator,
"Directory: " + directory_name,
separator]
+ descriptions
+ [separator,
"The length of the video is " + str(counter_seconds / 60.0) + " minute(s)",
"It should be under 202-205min (this is a pydub limitation)",
separator,
"Description is " + str(descriptions_len) + " characters long",
"It should be under 4500-5000 characters long (this is a youtube limitation)",
separator,
"Started " + str(start_time) + ", completed " + str(datetime.datetime.now()),
separator]
for d_line in descriptions:
print (d_line)
with codecs.open(descriptions_file_path, 'w', encoding = file_encoding) as the_file:
the_file.writelines(d_line + "n" for d_line in (descriptions))
def get_audio_description_subtitles_simple(counter_audio, audio_mediainfo):
title = audio_mediainfo['title'].strip().replace('\', '')
track_name = 'Track ' + str(counter_audio) + ": " + title
artist_name = audio_mediainfo['artist'].strip()
desc = track_name + " by " + artist_name
return desc, [track_name, artist_name]
def dry_run_get_audio_description_subtitles_dry_run(counter_audio, audio_mediainfo):
print(audio_mediainfo)
return "", ""
if __name__ == '__main__':
make_video( directory_name = os.path.expanduser('~/Music/LouisXIII copy'),
#artist_override = 'Dalida',
func_get_audio_description_subtitles = get_audio_description_subtitles_simple,
description_intro = ['Intended for personal use. I own the CDs', ''],
dry_run = True)
python python-3.x opencv video
python python-3.x opencv video
edited Apr 27 '18 at 2:33
Jamal♦
30.5k11121227
30.5k11121227
asked Apr 3 '18 at 22:09
Yulia VYulia V
267210
267210
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
$begingroup$
import os, codecs, datetime, glob
E401 multiple imports on one line
Recommend you run $ flake8
and heed its advice,
as PEP-8 asks for just one import per line.
Use isort
to organize them.
Each of your functions has lovely comments; thank you.
Recommend you turn the one-sentence comments into docstrings.
The add_subtitles()
function is maybe slightly long,
and could be broken out into one or two helpers.
The arg list is on the long side.
Width
+ height
could trivially be collapsed into size
,
but I wonder if some of the other attributes, like colour,
might sensibly be defaulted from an object that has an add_subtitles()
method.
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
A more pythonic way to express this would be to
construct a list of font sizes, and then
assign max( ... )
of those sizes.
Similarly, please assign sub_bg_right
as max
of two numbers.
It feels like much of this logic could sensibly be encapsulated
within a sub_bg
object.
draw.text(..., font = font)
PEP-8 asks for spaces around =
assignment, but no spaces around =
keyword args: font=font
.
Nice comments on make_video()
.
Again, it takes quite a few args.
It feels like a subtitle object could encapsulate several of them.
These args are trouble:
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
Well, file_encoding is fine, just lose the extra blanks around =
equals.
But the lists are trouble.
Now, I know you're not mutating them.
But it's a gotcha, evaluating and binding a mutable list at function definition time.
Don't get in the habit of doing that.
Make it default to an immutable sequence, such as a (tuple),
or use the usual idiom:
def foo(name, extensions=None):
if extensions is None:
extensions = ['jpg', 'png']
The point is to re-evaluate the assignment on each execution of foo()
,
rather than binding an immortal list just once.
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
Feel free to save one line by specifying , exist_ok=True
.
You could easily break out a few helpers from this function,
for example the whole audio_filenames
loop is naturally a helper function.
if not ('artist' in audio_mediainfo):
That's fine, but testing if 'artist' not in audio_mediainfo:
is
slightly more pythonic.
if (artist_override != None):
- No need for
(
extra parens)
in a pythonif
.
Please test
is
identity of theNone
singleton, rather than equality:if artist_override is not None:
. Or, more simply:if artist_override:
while (counter_seconds <= limit_audio_length_so_far):
No (
extra parens )
in a python while
, please.
if video_title == None:
Please test is None
.
"It should be under 202-205min (this is a pydub limitation)",
...
"It should be under 4500-5000 characters long (this is a youtube limitation)",
There are limits, but you're not telling me what they are.
Put a stake in the ground, say 202 min. and 4500 char. and be done with it.
title = audio_mediainfo['title'].strip().replace('\', '')
This is apparently sanitizing a filename, making it safe to open for write.
That is, you are rejecting Known Bad unicode code points.
I would be more convinced if you instead used maketrans and translate to
accept Known Good characters.
You tend to accommodate long expressions with backwhack continuation characters.
Consider using (
parens )
instead:
short_string = 'Hi!'
much_longer_string = ('This,'
' that,'
' and the other.')
$endgroup$
add a comment |
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f191204%2fconverting-mp3-albums-into-mp4-videos-for-youtube%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
import os, codecs, datetime, glob
E401 multiple imports on one line
Recommend you run $ flake8
and heed its advice,
as PEP-8 asks for just one import per line.
Use isort
to organize them.
Each of your functions has lovely comments; thank you.
Recommend you turn the one-sentence comments into docstrings.
The add_subtitles()
function is maybe slightly long,
and could be broken out into one or two helpers.
The arg list is on the long side.
Width
+ height
could trivially be collapsed into size
,
but I wonder if some of the other attributes, like colour,
might sensibly be defaulted from an object that has an add_subtitles()
method.
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
A more pythonic way to express this would be to
construct a list of font sizes, and then
assign max( ... )
of those sizes.
Similarly, please assign sub_bg_right
as max
of two numbers.
It feels like much of this logic could sensibly be encapsulated
within a sub_bg
object.
draw.text(..., font = font)
PEP-8 asks for spaces around =
assignment, but no spaces around =
keyword args: font=font
.
Nice comments on make_video()
.
Again, it takes quite a few args.
It feels like a subtitle object could encapsulate several of them.
These args are trouble:
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
Well, file_encoding is fine, just lose the extra blanks around =
equals.
But the lists are trouble.
Now, I know you're not mutating them.
But it's a gotcha, evaluating and binding a mutable list at function definition time.
Don't get in the habit of doing that.
Make it default to an immutable sequence, such as a (tuple),
or use the usual idiom:
def foo(name, extensions=None):
if extensions is None:
extensions = ['jpg', 'png']
The point is to re-evaluate the assignment on each execution of foo()
,
rather than binding an immortal list just once.
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
Feel free to save one line by specifying , exist_ok=True
.
You could easily break out a few helpers from this function,
for example the whole audio_filenames
loop is naturally a helper function.
if not ('artist' in audio_mediainfo):
That's fine, but testing if 'artist' not in audio_mediainfo:
is
slightly more pythonic.
if (artist_override != None):
- No need for
(
extra parens)
in a pythonif
.
Please test
is
identity of theNone
singleton, rather than equality:if artist_override is not None:
. Or, more simply:if artist_override:
while (counter_seconds <= limit_audio_length_so_far):
No (
extra parens )
in a python while
, please.
if video_title == None:
Please test is None
.
"It should be under 202-205min (this is a pydub limitation)",
...
"It should be under 4500-5000 characters long (this is a youtube limitation)",
There are limits, but you're not telling me what they are.
Put a stake in the ground, say 202 min. and 4500 char. and be done with it.
title = audio_mediainfo['title'].strip().replace('\', '')
This is apparently sanitizing a filename, making it safe to open for write.
That is, you are rejecting Known Bad unicode code points.
I would be more convinced if you instead used maketrans and translate to
accept Known Good characters.
You tend to accommodate long expressions with backwhack continuation characters.
Consider using (
parens )
instead:
short_string = 'Hi!'
much_longer_string = ('This,'
' that,'
' and the other.')
$endgroup$
add a comment |
$begingroup$
import os, codecs, datetime, glob
E401 multiple imports on one line
Recommend you run $ flake8
and heed its advice,
as PEP-8 asks for just one import per line.
Use isort
to organize them.
Each of your functions has lovely comments; thank you.
Recommend you turn the one-sentence comments into docstrings.
The add_subtitles()
function is maybe slightly long,
and could be broken out into one or two helpers.
The arg list is on the long side.
Width
+ height
could trivially be collapsed into size
,
but I wonder if some of the other attributes, like colour,
might sensibly be defaulted from an object that has an add_subtitles()
method.
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
A more pythonic way to express this would be to
construct a list of font sizes, and then
assign max( ... )
of those sizes.
Similarly, please assign sub_bg_right
as max
of two numbers.
It feels like much of this logic could sensibly be encapsulated
within a sub_bg
object.
draw.text(..., font = font)
PEP-8 asks for spaces around =
assignment, but no spaces around =
keyword args: font=font
.
Nice comments on make_video()
.
Again, it takes quite a few args.
It feels like a subtitle object could encapsulate several of them.
These args are trouble:
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
Well, file_encoding is fine, just lose the extra blanks around =
equals.
But the lists are trouble.
Now, I know you're not mutating them.
But it's a gotcha, evaluating and binding a mutable list at function definition time.
Don't get in the habit of doing that.
Make it default to an immutable sequence, such as a (tuple),
or use the usual idiom:
def foo(name, extensions=None):
if extensions is None:
extensions = ['jpg', 'png']
The point is to re-evaluate the assignment on each execution of foo()
,
rather than binding an immortal list just once.
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
Feel free to save one line by specifying , exist_ok=True
.
You could easily break out a few helpers from this function,
for example the whole audio_filenames
loop is naturally a helper function.
if not ('artist' in audio_mediainfo):
That's fine, but testing if 'artist' not in audio_mediainfo:
is
slightly more pythonic.
if (artist_override != None):
- No need for
(
extra parens)
in a pythonif
.
Please test
is
identity of theNone
singleton, rather than equality:if artist_override is not None:
. Or, more simply:if artist_override:
while (counter_seconds <= limit_audio_length_so_far):
No (
extra parens )
in a python while
, please.
if video_title == None:
Please test is None
.
"It should be under 202-205min (this is a pydub limitation)",
...
"It should be under 4500-5000 characters long (this is a youtube limitation)",
There are limits, but you're not telling me what they are.
Put a stake in the ground, say 202 min. and 4500 char. and be done with it.
title = audio_mediainfo['title'].strip().replace('\', '')
This is apparently sanitizing a filename, making it safe to open for write.
That is, you are rejecting Known Bad unicode code points.
I would be more convinced if you instead used maketrans and translate to
accept Known Good characters.
You tend to accommodate long expressions with backwhack continuation characters.
Consider using (
parens )
instead:
short_string = 'Hi!'
much_longer_string = ('This,'
' that,'
' and the other.')
$endgroup$
add a comment |
$begingroup$
import os, codecs, datetime, glob
E401 multiple imports on one line
Recommend you run $ flake8
and heed its advice,
as PEP-8 asks for just one import per line.
Use isort
to organize them.
Each of your functions has lovely comments; thank you.
Recommend you turn the one-sentence comments into docstrings.
The add_subtitles()
function is maybe slightly long,
and could be broken out into one or two helpers.
The arg list is on the long side.
Width
+ height
could trivially be collapsed into size
,
but I wonder if some of the other attributes, like colour,
might sensibly be defaulted from an object that has an add_subtitles()
method.
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
A more pythonic way to express this would be to
construct a list of font sizes, and then
assign max( ... )
of those sizes.
Similarly, please assign sub_bg_right
as max
of two numbers.
It feels like much of this logic could sensibly be encapsulated
within a sub_bg
object.
draw.text(..., font = font)
PEP-8 asks for spaces around =
assignment, but no spaces around =
keyword args: font=font
.
Nice comments on make_video()
.
Again, it takes quite a few args.
It feels like a subtitle object could encapsulate several of them.
These args are trouble:
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
Well, file_encoding is fine, just lose the extra blanks around =
equals.
But the lists are trouble.
Now, I know you're not mutating them.
But it's a gotcha, evaluating and binding a mutable list at function definition time.
Don't get in the habit of doing that.
Make it default to an immutable sequence, such as a (tuple),
or use the usual idiom:
def foo(name, extensions=None):
if extensions is None:
extensions = ['jpg', 'png']
The point is to re-evaluate the assignment on each execution of foo()
,
rather than binding an immortal list just once.
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
Feel free to save one line by specifying , exist_ok=True
.
You could easily break out a few helpers from this function,
for example the whole audio_filenames
loop is naturally a helper function.
if not ('artist' in audio_mediainfo):
That's fine, but testing if 'artist' not in audio_mediainfo:
is
slightly more pythonic.
if (artist_override != None):
- No need for
(
extra parens)
in a pythonif
.
Please test
is
identity of theNone
singleton, rather than equality:if artist_override is not None:
. Or, more simply:if artist_override:
while (counter_seconds <= limit_audio_length_so_far):
No (
extra parens )
in a python while
, please.
if video_title == None:
Please test is None
.
"It should be under 202-205min (this is a pydub limitation)",
...
"It should be under 4500-5000 characters long (this is a youtube limitation)",
There are limits, but you're not telling me what they are.
Put a stake in the ground, say 202 min. and 4500 char. and be done with it.
title = audio_mediainfo['title'].strip().replace('\', '')
This is apparently sanitizing a filename, making it safe to open for write.
That is, you are rejecting Known Bad unicode code points.
I would be more convinced if you instead used maketrans and translate to
accept Known Good characters.
You tend to accommodate long expressions with backwhack continuation characters.
Consider using (
parens )
instead:
short_string = 'Hi!'
much_longer_string = ('This,'
' that,'
' and the other.')
$endgroup$
import os, codecs, datetime, glob
E401 multiple imports on one line
Recommend you run $ flake8
and heed its advice,
as PEP-8 asks for just one import per line.
Use isort
to organize them.
Each of your functions has lovely comments; thank you.
Recommend you turn the one-sentence comments into docstrings.
The add_subtitles()
function is maybe slightly long,
and could be broken out into one or two helpers.
The arg list is on the long side.
Width
+ height
could trivially be collapsed into size
,
but I wonder if some of the other attributes, like colour,
might sensibly be defaulted from an object that has an add_subtitles()
method.
if max_length_subtitles < sub_size[0]:
max_length_subtitles = sub_size[0]
A more pythonic way to express this would be to
construct a list of font sizes, and then
assign max( ... )
of those sizes.
Similarly, please assign sub_bg_right
as max
of two numbers.
It feels like much of this logic could sensibly be encapsulated
within a sub_bg
object.
draw.text(..., font = font)
PEP-8 asks for spaces around =
assignment, but no spaces around =
keyword args: font=font
.
Nice comments on make_video()
.
Again, it takes quite a few args.
It feels like a subtitle object could encapsulate several of them.
These args are trouble:
description_intro = [''],
file_encoding = 'utf-8',
image_extensions = ['jpg', 'png'],
audio_extensions = ['mp3', 'wav'],
Well, file_encoding is fine, just lose the extra blanks around =
equals.
But the lists are trouble.
Now, I know you're not mutating them.
But it's a gotcha, evaluating and binding a mutable list at function definition time.
Don't get in the habit of doing that.
Make it default to an immutable sequence, such as a (tuple),
or use the usual idiom:
def foo(name, extensions=None):
if extensions is None:
extensions = ['jpg', 'png']
The point is to re-evaluate the assignment on each execution of foo()
,
rather than binding an immortal list just once.
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
Feel free to save one line by specifying , exist_ok=True
.
You could easily break out a few helpers from this function,
for example the whole audio_filenames
loop is naturally a helper function.
if not ('artist' in audio_mediainfo):
That's fine, but testing if 'artist' not in audio_mediainfo:
is
slightly more pythonic.
if (artist_override != None):
- No need for
(
extra parens)
in a pythonif
.
Please test
is
identity of theNone
singleton, rather than equality:if artist_override is not None:
. Or, more simply:if artist_override:
while (counter_seconds <= limit_audio_length_so_far):
No (
extra parens )
in a python while
, please.
if video_title == None:
Please test is None
.
"It should be under 202-205min (this is a pydub limitation)",
...
"It should be under 4500-5000 characters long (this is a youtube limitation)",
There are limits, but you're not telling me what they are.
Put a stake in the ground, say 202 min. and 4500 char. and be done with it.
title = audio_mediainfo['title'].strip().replace('\', '')
This is apparently sanitizing a filename, making it safe to open for write.
That is, you are rejecting Known Bad unicode code points.
I would be more convinced if you instead used maketrans and translate to
accept Known Good characters.
You tend to accommodate long expressions with backwhack continuation characters.
Consider using (
parens )
instead:
short_string = 'Hi!'
much_longer_string = ('This,'
' that,'
' and the other.')
answered 7 mins ago
J_HJ_H
4,572132
4,572132
add a comment |
add a comment |
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f191204%2fconverting-mp3-albums-into-mp4-videos-for-youtube%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown