18 Commits
Author SHA1 Message Date
RandyTheSilly c32b16598a Drop screen as a dependency 2024-03-24 17:37:53 -04:00
RandyTheSilly 3865e91517 Remove use of shell=True with Popen 2024-03-24 16:41:29 -04:00
RandyTheSilly 7411af721d Query /proc for process count instead of relying on the "ps" command (fixes Busybox compatibility) 2024-03-24 15:51:59 -04:00
RandyTheSilly ee3e1f9fa1 Support files with capitalized extensions 2024-03-23 12:35:36 -04:00
RandyTheSilly 91cea901be Improved non-FLAC detection 2023-07-02 18:41:05 -04:00
RandyTheSilly 1d59ed62ac Set correct permissions on license file 2023-06-13 15:45:57 -04:00
RandyTheSilly 572b997286 Removed pycache, added easy commit script 2023-06-13 15:41:47 -04:00
RandyTheSilly 6d8d6f876a Update README.md 2023-02-12 19:57:38 -05:00
RandyTheSilly 65dc1157e1 Update copyright date 2023-02-12 19:54:28 -05:00
RandyTheSilly 771f540af2 Indicate which file is currently being processed 2023-02-12 19:52:29 -05:00
RandyTheSilly 4055963026 Removed unnecessary EOF exits 2023-02-12 19:39:32 -05:00
RandyTheSilly af8c030201 Added a script to convert embedded album art to iPod-ready JPEGs 2023-02-12 19:37:01 -05:00
RandyTheSilly c7133060a0 Added an option to preserve album art from input files 2023-02-12 17:11:25 -05:00
RandyTheSilly a2e0e3fe74 Create bin directory when packaging (for symlink) 2022-11-16 20:25:18 -05:00
RandyTheSilly d0d34a04ee Scan tracks as individuals if there are mismatched channels 2022-11-16 20:11:37 -05:00
RandyTheSilly e78a0bc2c2 Update packaging, now installs to /usr/lib/flac2pod 2022-11-16 15:02:00 -05:00
RandyTheSilly 2fd4d685e0 Switch from subprocess.popen to subprocess.run where applicable, fix shebangs 2022-11-16 14:36:54 -05:00
RandyTheSilly 6732731c11 Fixed non-FLAC file copy function 2022-05-19 02:11:36 -04:00
10 changed files with 220 additions and 156 deletions
+21 -5
View File
@@ -1,24 +1,42 @@
# flac2pod
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 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.
# WARNING
This is an older project of mine that is only updated to fix bugs and maintain compatibility. It is written rather poorly, but it does what it says on the tin, so I will not be investing time into rewriting it.
# Usage
Simply run...
```
flac2pod <source_dir> <destination_dir>
```
...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.
...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:
@@ -26,8 +44,6 @@ source_dir -> artist dir -> album dir -> FLAC files
# 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/
- install flac, python-mutagen, ffmpeg, and screen
- start converting
If installing on a non-Arch-based distribution, a generic package is available on the releases page.
-70
View File
@@ -1,70 +0,0 @@
#!/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)
-1
View File
@@ -1 +0,0 @@
/usr/bin/flac2pod
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
git add -f LICENSE README.md commit.sh package.sh share lib/flac2pod-artconvert.py lib/flac2pod-flacgain.py lib/flac2pod.py
git commit -m "$1"
git push
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()
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
from argparse import ArgumentParser
from flac2pod import scan_source
from glob import glob
from mutagen.flac import FLAC, FLACNoHeaderError
from os import listdir
from subprocess import PIPE, run
from sys import exit as s_exit
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]):
for song in listdir(album):
gain = 1 # reset for each song
if not song.lower().endswith('.flac'):
gain = 2 # set non-flac signal
else:
try:
audio = FLAC(f"{album}/{song}")
if audio.get('replaygain_track_gain') or audio.get('REPLAYGAIN_TRACK_GAIN'):
print(f"[{album}/*] ReplayGain data is already present.")
break
else:
gain = 0
except FLACNoHeaderError:
gain = 2 # set non-flac signal
if gain == 2:
print(f"[{album}/*] Contains non-FLAC files, skipping album...")
break
if gain != 2 and (args.force or gain == 0):
print(f"[{album}/*] Adding ReplayGain data...")
album_files = glob(album + '/*')
run(['metaflac', '--remove-replay-gain'] + album_files)
output = run(['metaflac', '--add-replay-gain'] + album_files, stderr=PIPE, text=True)
if output.returncode == 1:
if output.stderr.__contains__('sample') or output.stderr.__contains__('resolution of') or output.\
stderr.__contains__('does not match'):
print('Resolution/Sample Rate/Channel mismatch, scanning tracks as individuals...')
for song in listdir(album):
print(f"[{album}/{song}] Adding ReplayGain data...")
output = run(('metaflac', '--add-replay-gain', f"{album}/{song}"), stderr=PIPE, text=True)
if output.returncode == 1:
print('There was an error processing your files.')
print(f"Return Code: [{str(output.returncode)}] {output.stderr}")
s_exit(1)
else:
print('There was an error processing your files.')
print(f"Return Code: [{str(output.returncode)}] {output.stderr}")
s_exit(1)
+64 -52
View File
@@ -1,19 +1,16 @@
#!/bin/python3
# external modules
#!/usr/bin/env python3
from argparse import ArgumentParser
from mutagen.flac import FLAC, FLACNoHeaderError
from os import cpu_count, path, walk
from mutagen.flac import FLAC
from os import cpu_count, getuid, listdir, path, stat, walk
from pathlib import Path
from shutil import copy
from subprocess import check_output, Popen
from subprocess import DEVNULL, Popen
from sys import exit as s_exit
from time import sleep
# utility functions
# scans source directory for music files, returns lists of album directories and their full paths
def scan_source(_source):
_artist_dirs, _album_dirs, _full_paths = [], [], []
for _root, _directories, _files in walk(path.expanduser(_source)):
@@ -29,39 +26,55 @@ def scan_source(_source):
for _album in sorted(_album_dirs):
for _root, _directories, _files in walk(path.expanduser(_album)):
for _filename in _files:
if _filename.endswith('.flac'):
if _filename.lower().endswith('.flac') or _filename.lower().endswith('.mp3') or \
_filename.lower().endswith('.mp4') or _filename.lower().endswith('.m4a'):
_full_paths.append(f"{_album}/{_filename}")
return _album_dirs, _full_paths
# creates destination directory structure
def create_destination():
for _dir in source_directories[0]:
Path(path.expanduser(_dir.replace(path.expanduser(source), path.expanduser(destination))))\
.mkdir(0o700, parents=True, exist_ok=True)
# counts how many ffmpeg processes are running under the current user
def count_user_ffmpeg_processes():
_user = getuid()
_proc_directory = '/proc'
_running_processes = 0
_subdirectories = [_d for _d in listdir(_proc_directory) if path.isdir(path.join(_proc_directory, _d))]
for _subdirectory in _subdirectories:
if _subdirectory.isnumeric():
if stat(_proc_directory + '/' + _subdirectory).st_uid == _user:
with open(_proc_directory + '/' + _subdirectory + '/cmdline', 'r') as _f:
if _f.read().startswith('ffmpeg'):
_running_processes += 1
return _running_processes
def scan_destination_convert():
_i, _i_total, _total_files = 0, 0, len(source_directories[1])
for _file in source_directories[1]:
_i_total += 1
_progress = round((_i_total / _total_files) * 100, 2)
if not Path(f"{(_file.replace(path.expanduser(source), path.expanduser(destination)))[:-5]}.m4a").is_file():
_file_s = _file.replace(' ', '\\ ').replace("'", "\\'").replace(')', '\\)').replace('(', '\\(')\
.replace(']', '\\]').replace('[', '\\[').replace('&', '\\&').replace('`', '\\`')\
.replace('$', '\\$')
_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():
if not Path(f"{(_file.replace(path.expanduser(source), path.expanduser(destination)))[:-5]}.m4a").is_file()\
and not Path(f"{(_file.replace(path.expanduser(source), path.expanduser(destination)))}").is_file():
if not _file.lower().endswith('.flac'):
print(f"[{_file}] Not a FLAC file, copying...")
copy(f"{_file}", f"{_file.replace(path.expanduser(source), path.expanduser(destination))}")
else:
_file_d = _file.replace(path.expanduser(source), path.expanduser(destination))[:-5] + '.m4a'
_active_processes = count_user_ffmpeg_processes()
while _active_processes >= cpu_count():
print(f"\nThere are already {cpu_count()} processes running...\n")
print(f"\nThere are already {_active_processes} processes running on your {cpu_count()} "
f"threads...\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:
_active_processes = count_user_ffmpeg_processes()
_i += 1
print(f"\n{_progress}% | \u001b[38;5;0;48;5;15mConverting {_file} to {_file_d}...\u001b[0m\n")
# get relevant ReplayGain info
_audio = FLAC(f"{_file}")
if args.albumgain:
if _audio.get('REPLAYGAIN_ALBUM_GAIN'):
@@ -89,33 +102,32 @@ def scan_destination_convert():
_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
# 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 = [], ['-metadata', f"comment=FLAC2PODRG#{_rggain}#{_rgpeak}#"]
else:
_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
_cmd = ['ffmpeg', '-i', _file] + _art_args + ['-c:a', 'aac', '-b:a', '256k'] + _bake_args +\
_flac2pod_rg_tags + ['-aac_pns', '0', '-movflags', '+faststart', _file_d]
if _rggain is not None:
print(f"\u001b[38;5;0;48;5;15mGain adjustment: {_rggain}dB\u001b[0m")
else:
print('\u001b[38;5;0;48;5;88mNo ReplayGain data found!\u001b[0m')
if args.stopifnogain:
s_exit(1)
Popen(_cmd, stdout=DEVNULL, stderr=DEVNULL)
_active_processes = count_user_ffmpeg_processes()
while _active_processes != 0:
print('\nPlease wait for the remaining conversions to complete...\n')
print(f"\nPlease wait for the remaining {_active_processes} conversions to complete...\n")
sleep(1)
_active_processes = int(check_output('ps -C ffmpeg | wc -l', shell=True)) - 1
sleep(1)
_active_processes = count_user_ffmpeg_processes()
print('\niPod conversion complete!\n')
s_exit(0)
@@ -129,11 +141,13 @@ if __name__ == "__main__":
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')
help='bake (encode) ReplayGain values into output files')
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')
parser.add_argument('-p', '--preserve', action='store_true',
help='copy album art from input files')
args = parser.parse_args()
if args.source_dir[0].endswith('/'):
@@ -148,5 +162,3 @@ if __name__ == "__main__":
source_directories = scan_source(source)
create_destination()
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'
read -r -p "Version number: " version
read -r -p "Revision number: " revision
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
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
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
fi
# Archive creation
echo -e '\nPackaging as generic...\n'
mkdir -p packages/archtemp/usr
cp -r bin packages/archtemp/usr/
cp -r share packages/archtemp/usr/
tar -C packages/archtemp -cvf packages/flac2pod-"$version".tar.xz usr/
rm -rf packages/archtemp
sha512="$(sha512sum packages/flac2pod-"$version".tar.xz | awk '{print $1;}')"
# generic packaging
echo -e '\npackaging as generic...\n'
mkdir -p output/generictemp/usr/{bin,lib/flac2pod}
cp -r lib/. output/generictemp/usr/lib/flac2pod/
ln -s /usr/lib/flac2pod/flac2pod.py output/generictemp/usr/bin/flac2pod
ln -s /usr/lib/flac2pod/flac2pod-artconvert.py output/generictemp/usr/bin/flac2pod-artconvert
ln -s /usr/lib/flac2pod/flac2pod-flacgain.py output/generictemp/usr/bin/flac2pod-flacgain
cp -r share output/generictemp/usr/
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 "\nGeneric packaging complete.\n"
echo -e "\ngeneric packaging complete\n"
# PKGBUILD creation
echo -e '\nGenerating PKGBUILD...'
echo -e '\ngenerating PKGBUILD...'
echo "# Maintainer: Randall Winkhart <idgr at tutanota dot com>
pkgname=flac2pod
pkgver="$version"
pkgrel="$revision"
@@ -39,15 +38,12 @@ pkgdesc='Converts your FLAC library to be iPod-ready'
url='https://github.com/rwinkhart/flac2pod'
arch=('any')
license=('GPL2')
depends=(ffmpeg flac python python-mutagen screen)
depends=(ffmpeg flac python python-mutagen python-pillow)
source=(\""$source"\")
sha512sums=('"$sha512"')
package() {
tar xf flac2pod-"\"\$pkgver\"".tar.xz -C "\"\${pkgdir}\""
}
" > packages/PKGBUILD
echo -e "\nPKGBUILD generated.\n"
" > output/PKGBUILD
echo -e "\nPKGBUILD generated\n"
Executable → Regular
+1 -1
View File
@@ -1,5 +1,5 @@
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
it under the terms of the GNU General Public License as published by