]> diplodocus.org Git - flac-archive/blob - fa-encode
Simplify (initial structure was minimal transformation of the Python)
[flac-archive] / fa-encode
1 #!/usr/local/bin/perl
2
3 =head1 NAME
4
5 B<fa-encode> - encode WAV files to FLAC
6
7 =head1 SYNOPSIS
8
9 B<fa-encode> [B<-d> I<artist-directory>] B<input-directory>
10
11 I<artist-directory> defaults to the album B<ARTIST> in the tags file; one of
12 these is required.
13
14 TODO: implement B<-d>
15
16 =cut
17
18 # POSIX.1-2017, Base Definitions, 3.282 Portable Filename Character Set says
19 # [A-Za-z0-9._-]
20 # https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
21
22 # Flac tags (set with `flac -T fieldname=...`) are Vorbis comments.
23 # https://www.xiph.org/vorbis/doc/v-comment.html#fieldnames field names we care about:
24 # - TITLE - Track/Work name
25 # - VERSION - may be used to differentiate multiple versions of the same track title in a single collection. (e.g. remix info)
26 # - ALBUM - The collection name to which this track belongs
27 # - TRACKNUMBER - The track number of this piece
28 # - ARTIST - The artist generally considered responsible for the track
29 # - DATE - Date the track was recorded (XXX I use US release date)
30 # https://age.hobba.nl/audio/mirroredpages/ogg-tagging.html (supposedly mirrored from http://reactor-core.org/ogg-tagging.html )
31 # specifies more:
32 # - DISCNUMBER - if part of a multi-disc album, put the disc number here
33 # - VERSION - e.g. "live", "radio edit"
34 # - PARTNUMBER - part number if a work is divided across tracks
35 # - PART - part name e.g. "Oh sole mio"
36 # https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
37 # - ALBUMARTIST - maps to ID3v2 TPE2
38
39 # Input is a directory containing:
40 # - trackNN.cdda.wav - WAV format files ripped from CD-DA audio tracks where NN is track number 01 - 99
41 # - tags - described below
42 # - cover.front.jpeg - optional JPEG format file containing the album front cover
43
44 # The tags file is composed of two blocks:
45 # 1. album tags
46 # 2. track tags
47 # Album tags may be:
48 # - ALBUM
49 # - ARTIST
50 # - DATE
51 # - DISCNUMBER (optional)
52
53 # Track tags may be any of the rest of the tags listed above, suffixed with
54 # [N] where N is the track number. ARTIST, and only ARTIST, may also appear
55 # in the track tags. In this case, it overrides the album artist. In order
56 # to add artists, the album artist must be listed again. For example, Reba
57 # McEntire's "The Heart Won't Lie" on the album "It's Your Call" features
58 # Vince Gill, and is specified as:
59 # ARTIST=Reba McEntire
60 # ALBUM=It's Your Call
61 # TITLE[5]=The Heart Won't Lie
62 # ARTIST[5]=Reba McEntire
63 # ARTIST[5]=Vince Gill
64
65 package epg::flac::archive::encode;
66
67 use v5.12;
68 use warnings;
69
70 use FindBin;
71
72 require "$FindBin::Bin/tags.p";
73 epg::flac::archive::tags->import(
74 qw[
75 read_tags
76 mangle_for_file_name
77 quote
78 ]);
79
80 sub main {
81 my $input_directory = shift;
82 my $fn = "$input_directory/tags";
83 open(my $fh, '<', $fn) || die("open($fn): $!");
84 my ($album, $tracks) = read_tags($fh);
85 if (!defined($album->{ALBUM}) || scalar(@{$album->{ALBUM}}) != 1) {
86 die('exactly one ALBUM tag required')
87 }
88 my $album_tag = $album->{ALBUM}->[0];
89 # TODO -d support
90 if (!defined($album->{ARTIST}) || scalar(@{$album->{ARTIST}}) != 1) {
91 die('exactly one ARTIST tag required')
92 }
93 my $artist = $album->{ARTIST}->[0];
94
95 my $date;
96 if (defined($album->{DATE})) {
97 if (scalar(@{$album->{DATE}}) != 1) {
98 die('one or zero DATE tags required')
99 }
100 $date = $album->{DATE}->[0];
101 }
102 my @discnumber = ();
103 if (defined($album->{DISCNUMBER})) {
104 if (scalar(@{$album->{DISCNUMBER}}) != 1) {
105 die('one or zero DISCNUMBER tags required')
106 }
107 @discnumber = ($album->{DISCNUMBER}->[0]);
108 }
109
110 my $dir = join('/', '..', mangle_for_file_name($artist), mangle_for_file_name($album_tag));
111
112 say('set -ex');
113 say('mkdir -p ', quote($dir));
114 my $tracknum = 0;
115 for my $track (@$tracks) {
116 $tracknum++;
117 if (!defined($track->{TITLE}) || scalar(@{$track->{TITLE}}) < 1) {
118 die("at least one TITLE required for track $tracknum")
119 }
120 $track->{ALBUM} = $album->{ALBUM};
121 $track->{TRACKNUMBER} = [$tracknum];
122 if (!defined($track->{ARTIST}) && defined($album->{ARTIST})) {
123 $track->{ARTIST} = $album->{ARTIST};
124 }
125 if (defined($date)) {
126 $track->{DATE} = [$date];
127 }
128 if (@discnumber) {
129 $track->{DISCNUMBER} = \@discnumber;
130 }
131 my $title = join(' ', @{$track->{TITLE}});
132 my $tracknum_s = sprintf('%02d', $tracknum);
133 my $fn = join('/',
134 $dir,
135 join('_',
136 (map { sprintf('%02d', $_) } @discnumber),
137 $tracknum_s,
138 mangle_for_file_name($title) . '.flac',
139 ),
140 );
141 -e $fn && die("cowardly refusing to clobber $fn");
142 my @pictures = ('--picture', quote('3|image/jpeg|||cover.front.jpeg')); # TODO optional
143 say(join(' ',
144 'flac -o',
145 quote($fn),
146 '--delete-input-file',
147 '-V',
148 '--no-padding',
149 '--best',
150 @pictures,
151 (map {
152 my $name = $_;
153 map {
154 ('-T', quote($name . '=' . $_))
155 } @{$track->{$_}}
156 } sort(keys(%$track))),
157 "track$tracknum_s.cdda.wav"
158 ));
159 }
160 say('rm tags cover.front.jpeg');
161
162 return 0;
163 }
164
165 if (!caller) {
166 exit(main(@ARGV))
167 }
168
169 1;