mirror of
https://github.com/rwinkhart/flac2pod.git
synced 2026-09-02 15:17:20 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c83969ce2 | ||
|
|
19a7f3b7b2 | ||
|
|
be7166e2dd | ||
|
|
8ba7815241 | ||
|
|
d03d73a2f8 | ||
|
|
aa9ec97695 | ||
|
|
fcee6ba3f4 | ||
|
|
1daa02f7ac | ||
|
|
6b8553cbee | ||
|
|
88e3994f4f | ||
|
|
f45dab8cfb | ||
|
|
6d8deda002 | ||
|
|
d2598c561a | ||
|
|
35bc1d9212 | ||
|
|
5550bc5b76 | ||
|
|
b91511286c | ||
|
|
8704749e7b | ||
|
|
094725cad0 | ||
|
|
f79d6ce025 | ||
|
|
c9a31dcbce | ||
|
|
1be96f0b20 | ||
|
|
282c193804 | ||
|
|
e833e4ce60 | ||
|
|
b5a2b9adb3 | ||
|
|
3c7ec3e921 | ||
|
|
10973e3ac8 | ||
|
|
c8ef53bc17 | ||
|
|
144c3f53e0 | ||
|
|
998c73c20f | ||
|
|
df1dce38eb | ||
|
|
ed12cc5d23 | ||
|
|
f72d3ac9b7 | ||
|
|
95a8dacd29 | ||
|
|
d64f4c56fa | ||
|
|
137e30c80b | ||
|
|
be5c96e182 | ||
|
|
7db318b561 | ||
|
|
dee1c3158f | ||
|
|
d973a3f659 | ||
|
|
d00e516e6c | ||
|
|
ea485480c4 | ||
|
|
b56dd7e5f5 |
@@ -1,27 +1,33 @@
|
|||||||
# flac2pod
|
# flac2pod
|
||||||
Seamlessly converts your existing FLAC library to be played on an iPod Classic.
|
Converts your existing FLAC library to be played on an iPod Classic.
|
||||||
|
|
||||||
This script makes use of ffmpeg to convert your existing FLAC music library to MP3-320 for easy iPod Classic use. It also takes advantage of "rg2sc" (https://github.com/rwinkhart/rg2sc) to convert any existing ReplayGain tags to Apple Sound Check tags that are compatible with Apple products.
|
This script makes use of ffmpeg to convert your existing FLAC music library to AAC-256 for easy iPod Classic use. It can also bake your existing ReplayGain data into the output files. SoundCheck support was removed due to this feature being very finicky on iPods. If you would like to add SoundCheck data to your output files, omit the --bake option and then use https://github.com/rwinkhart/rg2sc on your output files.
|
||||||
|
|
||||||
|
In order for ReplayGain baking to work, your FLAC library must already be tagged with ReplayGain data. If you haven't already done this, flac2pod comes with a script (flac2pod-flacgain) for quickly applying these ReplayGain tags.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
Simply run:
|
Simply run...
|
||||||
```
|
```
|
||||||
flac2pod
|
flac2pod <source_dir> <destination_dir>
|
||||||
```
|
```
|
||||||
...and you will be prompted for your source and destination directories.
|
...to convert your library, or run...
|
||||||
|
```
|
||||||
|
flac2pod --help
|
||||||
|
```
|
||||||
|
...to see additional options, or run...
|
||||||
|
```
|
||||||
|
flac2pod-flacgain <source_dir>
|
||||||
|
```
|
||||||
|
...to tag your FLAC library with ReplayGain data.
|
||||||
|
|
||||||
|
In order for flac2pod to function, your source_dir must be structured as follows:
|
||||||
|
|
||||||
In order for flac2pod to function, your library must be structured as follows:
|
source_dir -> artist dir -> album dir -> FLAC files
|
||||||
|
|
||||||
library parent dir -> artist dir -> album dir -> FLAC files
|
|
||||||
|
|
||||||
|
|
||||||
When prompted for the source directory, you want to input your library parent directory.
|
|
||||||
|
|
||||||
# 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". If installing on a non-Arch-based distribution, follow the manual installation instructions:
|
||||||
|
|
||||||
- copy the flac2pod executable from https://github.com/rwinkhart/flac2pod/blob/master/bin/flac2pod to your /usr/bin/
|
- copy the contents of https://github.com/rwinkhart/flac2pod/blob/master/bin/ to your /usr/bin/
|
||||||
- copy the rg2sc executable from https://github.com/rwinkhart/rg2sc/blob/master/bin/rg2sc to your /usr/bin
|
- install flac, python-mutagen, ffmpeg, and screen
|
||||||
- install python-mutagen, ffmpeg, and lame
|
- start converting
|
||||||
|
|||||||
+115
-26
@@ -2,17 +2,23 @@
|
|||||||
|
|
||||||
# external modules
|
# external modules
|
||||||
|
|
||||||
from os import path, system, walk
|
from argparse import ArgumentParser
|
||||||
|
from mutagen.flac import FLAC, FLACNoHeaderError
|
||||||
|
from os import cpu_count, path, walk
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from shutil import copy
|
||||||
|
from subprocess import check_output, Popen
|
||||||
|
from sys import exit as s_exit
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
# utility functions
|
# utility functions
|
||||||
|
|
||||||
def scan_source():
|
def scan_source(_source):
|
||||||
_artist_dirs, _album_dirs, _full_paths = [], [], []
|
_artist_dirs, _album_dirs, _full_paths = [], [], []
|
||||||
for _root, _directories, _files in walk(path.expanduser(source)):
|
for _root, _directories, _files in walk(path.expanduser(_source)):
|
||||||
for _dir in sorted(_directories):
|
for _dir in sorted(_directories):
|
||||||
if not _root.endswith(path.expanduser(source)):
|
if not _root.endswith(path.expanduser(_source)):
|
||||||
_artist_dirs.append(_root)
|
_artist_dirs.append(_root)
|
||||||
_artist_dirs = list(set(_artist_dirs))
|
_artist_dirs = list(set(_artist_dirs))
|
||||||
for _artist in sorted(_artist_dirs):
|
for _artist in sorted(_artist_dirs):
|
||||||
@@ -24,7 +30,7 @@ def scan_source():
|
|||||||
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'):
|
||||||
_full_paths.append(f"{_album}/{_filename[:-5]}")
|
_full_paths.append(f"{_album}/{_filename}")
|
||||||
return _album_dirs, _full_paths
|
return _album_dirs, _full_paths
|
||||||
|
|
||||||
|
|
||||||
@@ -34,30 +40,113 @@ def create_destination():
|
|||||||
.mkdir(0o700, parents=True, exist_ok=True)
|
.mkdir(0o700, parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def scan_destination_convert_mp3():
|
def scan_destination_convert():
|
||||||
|
_i, _i_total, _total_files = 0, 0, len(source_directories[1])
|
||||||
for _file in source_directories[1]:
|
for _file in source_directories[1]:
|
||||||
if not Path(f"{_file.replace(path.expanduser(source), path.expanduser(destination))}.mp3").is_file():
|
_i_total += 1
|
||||||
_file_s = _file.replace(' ', '\\ ').replace("'", "\\'") + '.flac'
|
_progress = round((_i_total / _total_files) * 100, 2)
|
||||||
_file_d = _file.replace(path.expanduser(source),
|
if not Path(f"{(_file.replace(path.expanduser(source), path.expanduser(destination)))[:-5]}.m4a").is_file():
|
||||||
path.expanduser(destination)).replace(' ', '\\ ').replace("'", "\\'") + '.mp3'
|
_file_s = _file.replace(' ', '\\ ').replace("'", "\\'").replace(')', '\\)').replace('(', '\\(')\
|
||||||
print(f"\n\u001b[38;5;0;48;5;15mConverting {_file_s} to {_file_d}...\u001b[0m\n")
|
.replace(']', '\\]').replace('[', '\\[').replace('&', '\\&').replace('`', '\\`')\
|
||||||
system(f"ffmpeg -i {_file_s} -c:v copy -ab 320k -map_metadata 0 -id3v2_version 3 {_file_d}")
|
.replace('$', '\\$')
|
||||||
rg2sc(_file_d)
|
_file_d = _file.replace(path.expanduser(source), path.expanduser(destination)).replace(' ', '\\ ')\
|
||||||
|
.replace("'", "\\'").replace(')', '\\)').replace('(', '\\(').replace(']', '\\]')\
|
||||||
|
.replace('[', '\\[').replace('&', '\\&').replace('`', '\\`').replace('$', '\\$')[:-5] + '.m4a'
|
||||||
|
_active_processes = int(check_output('ps -C ffmpeg | wc -l', shell=True)) - 1
|
||||||
|
if _active_processes >= cpu_count():
|
||||||
|
while _active_processes >= cpu_count():
|
||||||
|
print(f"\nThere are already {cpu_count()} processes running...\n")
|
||||||
|
sleep(1)
|
||||||
|
_active_processes = int(check_output('ps -C ffmpeg | wc -l', shell=True)) - 1
|
||||||
|
_i += 1
|
||||||
|
print(f"\n{_progress}% | \u001b[38;5;0;48;5;15mConverting {_file_s} to {_file_d}...\u001b[0m\n")
|
||||||
|
# get relevant ReplayGain info
|
||||||
|
try:
|
||||||
|
_audio = FLAC(f"{_file}")
|
||||||
|
if args.albumgain:
|
||||||
|
if _audio.get('REPLAYGAIN_ALBUM_GAIN'):
|
||||||
|
_rggain = float(_audio.get('REPLAYGAIN_ALBUM_GAIN')[0][:-3])
|
||||||
|
elif _audio.get('replaygain_album_gain'):
|
||||||
|
_rggain = float(_audio.get('replaygain_album_gain')[0][:-3])
|
||||||
|
else:
|
||||||
|
_rggain = None
|
||||||
|
if _audio.get('REPLAYGAIN_ALBUM_PEAK'):
|
||||||
|
_rgpeak = float(_audio.get('REPLAYGAIN_ALBUM_PEAK')[0])
|
||||||
|
elif _audio.get('replaygain_album_peak'):
|
||||||
|
_rgpeak = float(_audio.get('replaygain_album_peak')[0])
|
||||||
|
else:
|
||||||
|
_rgpeak = None
|
||||||
|
else:
|
||||||
|
if _audio.get('REPLAYGAIN_TRACK_GAIN'):
|
||||||
|
_rggain = float(_audio.get('REPLAYGAIN_TRACK_GAIN')[0][:-3])
|
||||||
|
elif _audio.get('replaygain_track_gain'):
|
||||||
|
_rggain = float(_audio.get('replaygain_track_gain')[0][:-3])
|
||||||
|
else:
|
||||||
|
_rggain = None
|
||||||
|
if _audio.get('REPLAYGAIN_TRACK_PEAK'):
|
||||||
|
_rgpeak = float(_audio.get('REPLAYGAIN_TRACK_PEAK')[0])
|
||||||
|
elif _audio.get('replaygain_track_peak'):
|
||||||
|
_rgpeak = float(_audio.get('replaygain_track_peak')[0])
|
||||||
|
else:
|
||||||
|
_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
|
||||||
|
if args.bake and _rggain is not None:
|
||||||
|
_bake_args, _flac2pod_rg_tags = '-filter:a "volume=' + str(_rggain) + 'dB"', ''
|
||||||
|
elif _rggain is not None and _rgpeak is not None:
|
||||||
|
_bake_args, _flac2pod_rg_tags = '', f"-metadata comment='FLAC2PODRG#{_rggain}#{_rgpeak}#'"
|
||||||
|
else:
|
||||||
|
_bake_args, _flac2pod_rg_tags = '', ''
|
||||||
|
# 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 " \
|
||||||
|
f"{_flac2pod_rg_tags} -aac_pns 0 -movflags +faststart -vn {_file_d} </dev/null"
|
||||||
|
if _rggain is not None:
|
||||||
|
print(f"\u001b[38;5;0;48;5;15mGain adjustment: {_rggain}dB\u001b[0m")
|
||||||
|
else:
|
||||||
|
print(f"\u001b[38;5;0;48;5;88mNo ReplayGain data found!\u001b[0m")
|
||||||
|
if args.stopifnogain:
|
||||||
|
s_exit(1)
|
||||||
|
Popen(_cmd, shell=True)
|
||||||
|
_active_processes = int(check_output('ps -C ffmpeg | wc -l', shell=True)) - 1
|
||||||
|
while _active_processes != 0:
|
||||||
|
print('\nPlease wait for the remaining conversions to complete...\n')
|
||||||
|
sleep(1)
|
||||||
|
_active_processes = int(check_output('ps -C ffmpeg | wc -l', shell=True)) - 1
|
||||||
|
sleep(1)
|
||||||
|
print('\niPod conversion complete!\n')
|
||||||
|
s_exit(0)
|
||||||
|
|
||||||
|
|
||||||
def rg2sc(_file):
|
# argument parsing
|
||||||
system(f"rg2sc -f {_file}")
|
if __name__ == "__main__":
|
||||||
|
parser = ArgumentParser(description='Convert your existing FLAC library to be played on an iPod Classic.')
|
||||||
|
parser.add_argument('source_dir', nargs='+',
|
||||||
|
help='directory tree containing music files - folder must contain library sorted into artists '
|
||||||
|
'and albums')
|
||||||
|
parser.add_argument('destination_dir', nargs='+',
|
||||||
|
help='destination parent directory for output - will be created if it does not already exist')
|
||||||
|
parser.add_argument('-b', '--bake', action='store_true',
|
||||||
|
help='bake (encode) ReplayGain values into the file')
|
||||||
|
parser.add_argument('-a', '--albumgain', action='store_true',
|
||||||
|
help='read ReplayGain album gain instead of ReplayGain track gain')
|
||||||
|
parser.add_argument('-x', '--stopifnogain', action='store_true',
|
||||||
|
help='stop flac2pod if ReplayGain data cannot be found in a file')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.source_dir[0].endswith('/'):
|
||||||
|
source = args.source_dir[0][:-1]
|
||||||
|
else:
|
||||||
|
source = args.source_dir[0]
|
||||||
|
if args.destination_dir[0].endswith('/'):
|
||||||
|
destination = args.destination_dir[0][:-1]
|
||||||
|
else:
|
||||||
|
destination = args.destination_dir[0]
|
||||||
|
|
||||||
# program start sequence
|
source_directories = scan_source(source)
|
||||||
|
create_destination()
|
||||||
|
scan_destination_convert()
|
||||||
|
|
||||||
source = input('Source parent directory: ')
|
s_exit(0)
|
||||||
if source.endswith('/'):
|
|
||||||
source = source[:-1]
|
|
||||||
destination = input('Destination parent directory: ')
|
|
||||||
if destination.endswith('/'):
|
|
||||||
destination = destination[:-1]
|
|
||||||
|
|
||||||
source_directories = scan_source()
|
|
||||||
create_destination()
|
|
||||||
scan_destination_convert_mp3()
|
|
||||||
|
|||||||
Executable
+70
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/python3
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from flac2pod import scan_source
|
||||||
|
from mutagen.flac import FLAC, FLACNoHeaderError
|
||||||
|
from os import listdir, system
|
||||||
|
from subprocess import PIPE, Popen, STDOUT
|
||||||
|
from sys import exit as s_exit
|
||||||
|
|
||||||
|
# argument parsing
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = ArgumentParser(description='Quickly tag your FLAC library with ReplayGain data using metaflac.')
|
||||||
|
parser.add_argument('source_dir', nargs='+',
|
||||||
|
help='directory tree containing music files - folder must contain library sorted into artists '
|
||||||
|
'and albums')
|
||||||
|
parser.add_argument('-f', '--force', action='store_true',
|
||||||
|
help='overwrite existing ReplayGain data')
|
||||||
|
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)
|
||||||
|
|
||||||
|
gain = 1
|
||||||
|
for album in sorted(source_directories[0]):
|
||||||
|
std_album = album.replace(' ', '\\ ').replace("'", "\\'").replace(')', '\\)').replace('(', '\\(')\
|
||||||
|
.replace(']', '\\]').replace('[', '\\[').replace('&', '\\&').replace('`', '\\`').replace('$', '\\$')
|
||||||
|
for song in listdir(album):
|
||||||
|
try:
|
||||||
|
audio = FLAC(f"{album}/{song}")
|
||||||
|
if audio.get('replaygain_track_gain') or audio.get('REPLAYGAIN_TRACK_GAIN'):
|
||||||
|
gain = 1
|
||||||
|
else:
|
||||||
|
gain = 0
|
||||||
|
break
|
||||||
|
except FLACNoHeaderError:
|
||||||
|
print(f"[{album}/*] Contains non-FLAC files, skipping album...")
|
||||||
|
gain = 2
|
||||||
|
if gain != 2 and (args.force or gain == 0):
|
||||||
|
print(f"[{album}/*] Adding ReplayGain data...")
|
||||||
|
system(f"metaflac --remove-replay-gain {std_album}/*")
|
||||||
|
command = f"metaflac --add-replay-gain {std_album}/*"
|
||||||
|
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 block.decode('utf-8').__contains__('sample') or block.decode('utf-8').__contains__('resolution of'):
|
||||||
|
print('Resolution/Sample Rate mismatch, scanning tracks as individuals...')
|
||||||
|
for song in listdir(album):
|
||||||
|
print(f"[{album}/{song}] Adding ReplayGain data...")
|
||||||
|
std_song = song.replace(' ', '\\ ').replace("'", "\\'").replace(')', '\\)')\
|
||||||
|
.replace('(', '\\(').replace('&', '\\&').replace('`', '\\`').replace('$', '\\$')
|
||||||
|
command = f"metaflac --add-replay-gain {std_album}/{std_song}"
|
||||||
|
output = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
|
||||||
|
block = output.communicate()[0].strip()
|
||||||
|
if output.returncode == 1:
|
||||||
|
print('There was an error processing your files.')
|
||||||
|
print(f"Return Code: [{str(output.returncode)}] {block.decode('utf-8')}")
|
||||||
|
s_exit(1)
|
||||||
|
else:
|
||||||
|
print('There was an error processing your files.')
|
||||||
|
print(f"Return Code: [{str(output.returncode)}] {block.decode('utf-8')}")
|
||||||
|
s_exit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"[{album}/*] ReplayGain data is already present.")
|
||||||
|
|
||||||
|
s_exit(0)
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/usr/bin/flac2pod
|
||||||
+1
-1
@@ -39,7 +39,7 @@ 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=(python ffmpeg lame rg2sc)
|
depends=(ffmpeg flac python python-mutagen screen)
|
||||||
|
|
||||||
source=(\""$source"\")
|
source=(\""$source"\")
|
||||||
sha512sums=('"$sha512"')
|
sha512sums=('"$sha512"')
|
||||||
|
|||||||
Reference in New Issue
Block a user