11 Commits
8 changed files with 133 additions and 93 deletions
+7 -5
View File
@@ -18,7 +18,11 @@ flac2pod --help
``` ```
flac2pod-flacgain <source_dir> flac2pod-flacgain <source_dir>
``` ```
...to tag your FLAC library with ReplayGain data. ...to tag your FLAC library with ReplayGain data, or run...
```
flac2pod-artconvert <source_dir>
```
...to convert embedded PNGs (in MP4/M4A files) to iPod-ready JPEGs.
In order for flac2pod to function, your source_dir must be structured as follows: In order for flac2pod to function, your source_dir must be structured as follows:
@@ -26,8 +30,6 @@ source_dir -> artist dir -> album dir -> FLAC files
# Installation # Installation
flac2pod is available in the AUR as "flac2pod". If installing on a non-Arch-based distribution, follow the manual installation instructions: flac2pod is available in the AUR as "flac2pod".
- copy the contents of https://github.com/rwinkhart/flac2pod/blob/master/bin/ to your /usr/bin/ If installing on a non-Arch-based distribution, a generic package is available on the releases page.
- install flac, python-mutagen, ffmpeg, and screen
- start converting
-1
View File
@@ -1 +0,0 @@
/usr/bin/flac2pod
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
from argparse import ArgumentParser
from flac2pod import scan_source
from mutagen import mp4
from mutagen.mp4 import MP4Cover
from os import listdir
from PIL import Image
if __name__ == "__main__":
parser = ArgumentParser(description='Convert embedded album art in MP4/M4A files from PNGs to iPod-compatible '
'JPEGs.')
parser.add_argument('source_dir', nargs='+',
help='directory tree containing music files - folder must contain library sorted into artists '
'and albums')
args = parser.parse_args()
if args.source_dir[0].endswith('/'):
source = args.source_dir[0][:-1]
else:
source = args.source_dir[0]
source_directories = scan_source(source)
for album in sorted(source_directories[0]):
for song in listdir(album):
audio = mp4.MP4(f"{album}/{song}")
if audio.tags is None:
continue
if "covr" not in audio.tags:
continue
cover = audio.tags["covr"][0]
print(f"[{album}/{song}] Converting...")
open("/tmp/cover.png", "wb").write(cover)
image = Image.open("/tmp/cover.png")
image.thumbnail((300, 300))
image.save("/tmp/cover.jpeg", "JPEG", quality=50)
with open("/tmp/cover.jpeg", "rb") as f:
audio["covr"] = [
MP4Cover(f.read(), imageformat=MP4Cover.FORMAT_JPEG)
]
audio.save()
@@ -1,13 +1,12 @@
#!/bin/python3 #!/usr/bin/env python3
from argparse import ArgumentParser from argparse import ArgumentParser
from flac2pod import scan_source from flac2pod import scan_source
from mutagen.flac import FLAC, FLACNoHeaderError from mutagen.flac import FLAC, FLACNoHeaderError
from os import listdir, system from os import listdir, system
from subprocess import PIPE, Popen, STDOUT from subprocess import PIPE, run
from sys import exit as s_exit from sys import exit as s_exit
# argument parsing
if __name__ == "__main__": if __name__ == "__main__":
parser = ArgumentParser(description='Quickly tag your FLAC library with ReplayGain data using metaflac.') parser = ArgumentParser(description='Quickly tag your FLAC library with ReplayGain data using metaflac.')
parser.add_argument('source_dir', nargs='+', parser.add_argument('source_dir', nargs='+',
@@ -42,29 +41,25 @@ if __name__ == "__main__":
if gain != 2 and (args.force or gain == 0): if gain != 2 and (args.force or gain == 0):
print(f"[{album}/*] Adding ReplayGain data...") print(f"[{album}/*] Adding ReplayGain data...")
system(f"metaflac --remove-replay-gain {std_album}/*") system(f"metaflac --remove-replay-gain {std_album}/*")
command = f"metaflac --add-replay-gain {std_album}/*" output = run(f"metaflac --add-replay-gain {std_album}/*", shell=True, stderr=PIPE, text=True)
output = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
block = output.communicate()[0].strip() # blocks the program from proceeding until stdout is given
if output.returncode == 1: if output.returncode == 1:
if block.decode('utf-8').__contains__('sample') or block.decode('utf-8').__contains__('resolution of'): if output.stderr.__contains__('sample') or output.stderr.__contains__('resolution of') or output.\
print('Resolution/Sample Rate mismatch, scanning tracks as individuals...') stderr.__contains__('does not match'):
print('Resolution/Sample Rate/Channel mismatch, scanning tracks as individuals...')
for song in listdir(album): for song in listdir(album):
print(f"[{album}/{song}] Adding ReplayGain data...") print(f"[{album}/{song}] Adding ReplayGain data...")
std_song = song.replace(' ', '\\ ').replace("'", "\\'").replace(')', '\\)')\ std_song = song.replace(' ', '\\ ').replace("'", "\\'").replace(')', '\\)')\
.replace('(', '\\(').replace('&', '\\&').replace('`', '\\`').replace('$', '\\$') .replace('(', '\\(').replace('&', '\\&').replace('`', '\\`').replace('$', '\\$')
command = f"metaflac --add-replay-gain {std_album}/{std_song}" output = run(f"metaflac --add-replay-gain {std_album}/{std_song}", shell=True, stderr=PIPE,
output = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) text=True)
block = output.communicate()[0].strip()
if output.returncode == 1: if output.returncode == 1:
print('There was an error processing your files.') print('There was an error processing your files.')
print(f"Return Code: [{str(output.returncode)}] {block.decode('utf-8')}") print(f"Return Code: [{str(output.returncode)}] {output.stderr}")
s_exit(1) s_exit(1)
else: else:
print('There was an error processing your files.') print('There was an error processing your files.')
print(f"Return Code: [{str(output.returncode)}] {block.decode('utf-8')}") print(f"Return Code: [{str(output.returncode)}] {output.stderr}")
s_exit(1) s_exit(1)
else: else:
print(f"[{album}/*] ReplayGain data is already present.") print(f"[{album}/*] ReplayGain data is already present.")
s_exit(0)
+25 -19
View File
@@ -1,9 +1,9 @@
#!/bin/python3 #!/usr/bin/env python3
# external modules # external modules
from argparse import ArgumentParser from argparse import ArgumentParser
from mutagen.flac import FLAC, FLACNoHeaderError from mutagen.flac import FLAC
from os import cpu_count, path, walk from os import cpu_count, path, walk
from pathlib import Path from pathlib import Path
from shutil import copy from shutil import copy
@@ -29,7 +29,8 @@ def scan_source(_source):
for _album in sorted(_album_dirs): for _album in sorted(_album_dirs):
for _root, _directories, _files in walk(path.expanduser(_album)): for _root, _directories, _files in walk(path.expanduser(_album)):
for _filename in _files: for _filename in _files:
if _filename.endswith('.flac'): if _filename.endswith('.flac') or _filename.endswith('.mp3') or _filename.endswith('.mp4') \
or _filename.endswith('.m4a'):
_full_paths.append(f"{_album}/{_filename}") _full_paths.append(f"{_album}/{_filename}")
return _album_dirs, _full_paths return _album_dirs, _full_paths
@@ -45,13 +46,19 @@ def scan_destination_convert():
for _file in source_directories[1]: for _file in source_directories[1]:
_i_total += 1 _i_total += 1
_progress = round((_i_total / _total_files) * 100, 2) _progress = round((_i_total / _total_files) * 100, 2)
if not Path(f"{(_file.replace(path.expanduser(source), path.expanduser(destination)))[:-5]}.m4a").is_file(): if not Path(f"{(_file.replace(path.expanduser(source), path.expanduser(destination)))[:-5]}.m4a").is_file()\
_file_s = _file.replace(' ', '\\ ').replace("'", "\\'").replace(')', '\\)').replace('(', '\\(')\ and not Path(f"{(_file.replace(path.expanduser(source), path.expanduser(destination)))}").is_file():
.replace(']', '\\]').replace('[', '\\[').replace('&', '\\&').replace('`', '\\`')\ if not _file.endswith('.flac'):
print(f"[{_file}] Not a FLAC file, copying...")
copy(f"{_file}", f"{_file.replace(path.expanduser(source), path.expanduser(destination))}")
else:
_file_s = _file.replace(' ', '\\ ').replace("'", "\\'").replace(')', '\\)').replace('(', '\\(') \
.replace(']', '\\]').replace('[', '\\[').replace('&', '\\&').replace('`', '\\`') \
.replace('$', '\\$') .replace('$', '\\$')
_file_d = _file.replace(path.expanduser(source), path.expanduser(destination)).replace(' ', '\\ ')\ _file_d = _file.replace(path.expanduser(source), path.expanduser(destination)).replace(' ', '\\ ') \
.replace("'", "\\'").replace(')', '\\)').replace('(', '\\(').replace(']', '\\]')\ .replace("'", "\\'").replace(')', '\\)').replace('(', '\\(').replace(']', '\\]') \
.replace('[', '\\[').replace('&', '\\&').replace('`', '\\`').replace('$', '\\$')[:-5] + '.m4a' .replace('[', '\\[').replace('&', '\\&').replace('`', '\\`').replace('$', '\\$')[
:-5] + '.m4a'
_active_processes = int(check_output('ps -C ffmpeg | wc -l', shell=True)) - 1 _active_processes = int(check_output('ps -C ffmpeg | wc -l', shell=True)) - 1
if _active_processes >= cpu_count(): if _active_processes >= cpu_count():
while _active_processes >= cpu_count(): while _active_processes >= cpu_count():
@@ -61,7 +68,6 @@ def scan_destination_convert():
_i += 1 _i += 1
print(f"\n{_progress}% | \u001b[38;5;0;48;5;15mConverting {_file_s} to {_file_d}...\u001b[0m\n") print(f"\n{_progress}% | \u001b[38;5;0;48;5;15mConverting {_file_s} to {_file_d}...\u001b[0m\n")
# get relevant ReplayGain info # get relevant ReplayGain info
try:
_audio = FLAC(f"{_file}") _audio = FLAC(f"{_file}")
if args.albumgain: if args.albumgain:
if _audio.get('REPLAYGAIN_ALBUM_GAIN'): if _audio.get('REPLAYGAIN_ALBUM_GAIN'):
@@ -89,10 +95,6 @@ def scan_destination_convert():
_rgpeak = float(_audio.get('replaygain_track_peak')[0]) _rgpeak = float(_audio.get('replaygain_track_peak')[0])
else: else:
_rgpeak = None _rgpeak = None
except FLACNoHeaderError:
print(f"[{_file}] Not a FLAC file, copying...")
copy(f"{_file}", f"{_file.replace(path.expanduser(source), path.expanduser(destination))}")
_rggain, _rgpeak = None, None
# determine ffmpeg arguments # determine ffmpeg arguments
if args.bake and _rggain is not None: if args.bake and _rggain is not None:
_bake_args, _flac2pod_rg_tags = '-filter:a "volume=' + str(_rggain) + 'dB"', '' _bake_args, _flac2pod_rg_tags = '-filter:a "volume=' + str(_rggain) + 'dB"', ''
@@ -100,9 +102,13 @@ def scan_destination_convert():
_bake_args, _flac2pod_rg_tags = '', f"-metadata comment='FLAC2PODRG#{_rggain}#{_rgpeak}#'" _bake_args, _flac2pod_rg_tags = '', f"-metadata comment='FLAC2PODRG#{_rggain}#{_rgpeak}#'"
else: else:
_bake_args, _flac2pod_rg_tags = '', '' _bake_args, _flac2pod_rg_tags = '', ''
if args.preserve:
_art_args = '-c:v copy -map_metadata 0:g'
else:
_art_args = '-vn'
# generate the appropriate ffmpeg command # generate the appropriate ffmpeg command
_cmd = f"screen -DmS flac2pod{_i} ffmpeg -i {_file_s} -c:a aac -ab 256k {_bake_args} -map_metadata 0 " \ _cmd = f"screen -DmS flac2pod{_i} ffmpeg -i {_file_s} {_art_args} -c:a aac -b:a 256k {_bake_args} " \
f"{_flac2pod_rg_tags} -aac_pns 0 -movflags +faststart -vn {_file_d} </dev/null" f"{_flac2pod_rg_tags} -aac_pns 0 -movflags +faststart {_file_d} </dev/null"
if _rggain is not None: if _rggain is not None:
print(f"\u001b[38;5;0;48;5;15mGain adjustment: {_rggain}dB\u001b[0m") print(f"\u001b[38;5;0;48;5;15mGain adjustment: {_rggain}dB\u001b[0m")
else: else:
@@ -129,11 +135,13 @@ if __name__ == "__main__":
parser.add_argument('destination_dir', nargs='+', parser.add_argument('destination_dir', nargs='+',
help='destination parent directory for output - will be created if it does not already exist') help='destination parent directory for output - will be created if it does not already exist')
parser.add_argument('-b', '--bake', action='store_true', parser.add_argument('-b', '--bake', action='store_true',
help='bake (encode) ReplayGain values into the file') help='bake (encode) ReplayGain values into output files')
parser.add_argument('-a', '--albumgain', action='store_true', parser.add_argument('-a', '--albumgain', action='store_true',
help='read ReplayGain album gain instead of ReplayGain track gain') help='read ReplayGain album gain instead of ReplayGain track gain')
parser.add_argument('-x', '--stopifnogain', action='store_true', parser.add_argument('-x', '--stopifnogain', action='store_true',
help='stop flac2pod if ReplayGain data cannot be found in a file') help='stop flac2pod if ReplayGain data cannot be found in a file')
parser.add_argument('-p', '--preserve', action='store_true',
help='copy album art from input files')
args = parser.parse_args() args = parser.parse_args()
if args.source_dir[0].endswith('/'): if args.source_dir[0].endswith('/'):
@@ -148,5 +156,3 @@ if __name__ == "__main__":
source_directories = scan_source(source) source_directories = scan_source(source)
create_destination() create_destination()
scan_destination_convert() scan_destination_convert()
s_exit(0)
+23 -27
View File
@@ -1,14 +1,12 @@
#!/bin/bash #!/usr/bin/env bash
# This script packages flac2pod (from source) for Arch Linux. echo -e '\n\nthe value entered in this field will only affect the version reported to the package manager - the latest source is used regardless\n'
read -r -p "version number: " version
read -r -p "revision number: " revision
echo -e '\n\nThe value entered in this field will only affect the version reported to the package manager. The latest source is used regardless.\n' echo -e '\noptions (please enter the number only):'
read -r -p "Version number: " version echo -e '\n1. gitHub release tag\n2. local\n'
read -r -p "Revision number: " revision read -r -p "source (for build scripts): " source
echo -e '\nOptions (please enter the number only):'
echo -e '\n1. GitHub Release Tag\n2. Local\n'
read -r -p "Source (for build scripts): " source
if [ "$source" == "1" ]; then if [ "$source" == "1" ]; then
source='https://github.com/rwinkhart/flac2pod/releases/download/v$pkgver/flac2pod-$pkgver.tar.xz' source='https://github.com/rwinkhart/flac2pod/releases/download/v$pkgver/flac2pod-$pkgver.tar.xz'
@@ -16,22 +14,23 @@ else
source=local://flac2pod-"$version".tar.xz source=local://flac2pod-"$version".tar.xz
fi fi
# generic packaging
# Archive creation echo -e '\npackaging as generic...\n'
echo -e '\nPackaging as generic...\n' mkdir -p output/generictemp/usr/{bin,lib/flac2pod}
mkdir -p packages/archtemp/usr cp -r lib/. output/generictemp/usr/lib/flac2pod/
cp -r bin packages/archtemp/usr/ ln -s /usr/lib/flac2pod/flac2pod.py output/generictemp/usr/bin/flac2pod
cp -r share packages/archtemp/usr/ ln -s /usr/lib/flac2pod/flac2pod-artconvert.py output/generictemp/usr/bin/flac2pod-artconvert
tar -C packages/archtemp -cvf packages/flac2pod-"$version".tar.xz usr/ ln -s /usr/lib/flac2pod/flac2pod-flacgain.py output/generictemp/usr/bin/flac2pod-flacgain
rm -rf packages/archtemp cp -r share output/generictemp/usr/
sha512="$(sha512sum packages/flac2pod-"$version".tar.xz | awk '{print $1;}')" tar -C output/generictemp -cvJf output/flac2pod-"$version".tar.xz usr/
rm -rf output/generictemp
sha512="$(sha512sum output/flac2pod-"$version".tar.xz | awk '{print $1;}')"
echo -e "\nsha512 sum:\n$sha512" echo -e "\nsha512 sum:\n$sha512"
echo -e "\nGeneric packaging complete.\n" echo -e "\ngeneric packaging complete\n"
# PKGBUILD creation # PKGBUILD creation
echo -e '\nGenerating PKGBUILD...' echo -e '\ngenerating PKGBUILD...'
echo "# Maintainer: Randall Winkhart <idgr at tutanota dot com> echo "# Maintainer: Randall Winkhart <idgr at tutanota dot com>
pkgname=flac2pod pkgname=flac2pod
pkgver="$version" pkgver="$version"
pkgrel="$revision" pkgrel="$revision"
@@ -39,15 +38,12 @@ pkgdesc='Converts your FLAC library to be iPod-ready'
url='https://github.com/rwinkhart/flac2pod' url='https://github.com/rwinkhart/flac2pod'
arch=('any') arch=('any')
license=('GPL2') license=('GPL2')
depends=(ffmpeg flac python python-mutagen screen) depends=(ffmpeg flac python python-mutagen python-pillow screen)
source=(\""$source"\") source=(\""$source"\")
sha512sums=('"$sha512"') sha512sums=('"$sha512"')
package() { package() {
tar xf flac2pod-"\"\$pkgver\"".tar.xz -C "\"\${pkgdir}\"" tar xf flac2pod-"\"\$pkgver\"".tar.xz -C "\"\${pkgdir}\""
} }
" > packages/PKGBUILD " > output/PKGBUILD
echo -e "\nPKGBUILD generated.\n" echo -e "\nPKGBUILD generated\n"
+1 -1
View File
@@ -1,5 +1,5 @@
flac2pod is a FOSS audio transcoding script for preparing music for an Apple iPod. flac2pod is a FOSS audio transcoding script for preparing music for an Apple iPod.
Copyright (C) 2022 Randall Winkhart idgr@tutanota.com Copyright (C) 2022-2023 Randall Winkhart idgr@tutanota.com
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by