]> diplodocus.org Git - flac-archive/blob - tags.pl
71e696882b59eb59b277afe5e80412d73aa3668e
[flac-archive] / tags.pl
1 package epg::flac::archive::tags;
2
3 =head1 NAME tags.pl
4
5 subroutines for tag processing
6
7 =head1 DESCRIPTION
8
9 TODO
10
11 =cut
12
13 # POSIX.1-2017, Base Definitions, 3.282 Portable Filename Character Set says
14 # [A-Za-z0-9._-]
15 # https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
16
17 # Flac tags (set with `flac -T fieldname=...`) are Vorbis comments.
18 # https://www.xiph.org/vorbis/doc/v-comment.html#fieldnames field names we care about:
19 # - TITLE - Track/Work name
20 # - VERSION - may be used to differentiate multiple versions of the same track title in a single collection. (e.g. remix info)
21 # - ALBUM - The collection name to which this track belongs
22 # - TRACKNUMBER - The track number of this piece
23 # - ARTIST - The artist generally considered responsible for the track
24 # - DATE - Date the track was recorded (XXX I use US release date)
25 # https://age.hobba.nl/audio/mirroredpages/ogg-tagging.html (supposedly mirrored from http://reactor-core.org/ogg-tagging.html )
26 # specifies more:
27 # - DISCNUMBER - if part of a multi-disc album, put the disc number here
28 # - VERSION - e.g. "live", "radio edit"
29 # - PARTNUMBER - part number if a work is divided across tracks
30 # - PART - part name e.g. "Oh sole mio"
31 # https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
32 # - ALBUMARTIST - maps to ID3v2 TPE2
33
34 # Input is a directory containing:
35 # - trackNN.cdda.wav - WAV format files ripped from CD-DA audio tracks where NN is track number 01 - 99
36 # - tags - described below
37 # - cover.front.jpeg - optional JPEG format file containing the album front cover
38
39 # The tags file is composed of two blocks:
40 # 1. album tags
41 # 2. track tags
42 # Album tags may be:
43 # - ALBUM
44 # - ARTIST
45 # - DATE
46 # - DISCNUMBER (optional)
47
48 # Track tags may be any of the rest of the tags listed above, suffixed with
49 # [N] where N is the track number. ARTIST, and only ARTIST, may also appear
50 # in the track tags. In this case, it overrides the album artist. In order
51 # to add artists, the album artist must be listed again. For example, Reba
52 # McEntire's "The Heart Won't Lie" on the album "It's Your Call" features
53 # Vince Gill, and is specified as:
54 # ARTIST=Reba McEntire
55 # ALBUM=It's Your Call
56 # TITLE[5]=The Heart Won't Lie
57 # ARTIST[5]=Reba McEntire
58 # ARTIST[5]=Vince Gill
59
60 use v5.12;
61 use warnings;
62
63 use Exporter 'import';
64
65 our @EXPORT_OK = qw[
66 disc_tags
67 read_tags
68 mangle_for_file_name
69 quote
70 two_digits
71 ];
72
73 sub two_digits {
74 my $n = shift;
75 my $s = sprintf('%02d', $n);
76 if (length($s) != 2) {
77 die("$n won't fit into two digits")
78 }
79 $s
80 }
81
82 sub read_tags {
83 my $fh = shift;
84 my %album;
85 my @tracks;
86 my $tracknum = 0;
87 while (<$fh>) {
88 chomp;
89 s/^([^=[\]]+)(\[(\d+)])?=// || die("no field name in $_");
90 my $name = $1;
91 # TODO validate $name
92 # TODO album tags should be illegal after track tags
93 if (defined($3)) {
94 if ($3 == $tracknum + 2) {
95 $tracknum++;
96 } elsif ($3 != $tracknum + 1) {
97 $tracknum++; # increment from 0 to 1 for error message
98 die("illegal track number jump from $tracknum to $3")
99 }
100 push(@{$tracks[$tracknum]->{$name}}, $_);
101 } else {
102 push(@{$album{$name}}, $_);
103 }
104 }
105
106 \%album, \@tracks
107 }
108
109 sub one_or_undef {
110 my $name = shift;
111 my $tags = shift;
112 my $tag = $tags->{$name};
113 if (defined($tag) && @$tag > 0) {
114 return $tag->[0]
115 }
116 undef
117 }
118
119 sub one {
120 my $tag = one_or_undef(@_);
121 if (!defined($tag)) {
122 my $name = shift;
123 die("exactly one $name tag required")
124 }
125 $tag
126 }
127
128 =over
129
130 =item B<disc_tags>
131
132 Interpret the tags hash for a disc, returning:
133
134 =over
135
136 =item *
137 Artist tag (exactly one required in the hash).
138
139 =item *
140 Album tag (exactly one required).
141
142 =item *
143 Date tag (zero or one required).
144
145 =item *
146 Disc number (zero or one required) as a reference to an array of size 0 or 1.
147
148 =back
149 =cut
150 sub disc_tags {
151 my %tags = @_;
152
153 if (!defined($tags{ALBUM}) || @{$tags{ALBUM}} != 1) {
154 die('exactly one ALBUM tag required')
155 }
156 my $album_tag = $tags{ALBUM}->[0];
157
158 # TODO ALBUMARTIST
159 if (!defined($tags{ARTIST}) || @{$tags{ARTIST}} != 1) {
160 die('exactly one ARTIST tag required')
161 }
162 my $artist = $tags{ARTIST}->[0];
163
164 my $date;
165 if (defined($tags{DATE})) {
166 if (@{$tags{DATE}} != 1) {
167 die('one or zero DATE tags required')
168 }
169 $date = $tags{DATE}->[0];
170 }
171
172 my @discnumber = ();
173 if (defined($tags{DISCNUMBER})) {
174 if (@{$tags{DISCNUMBER}} != 1) {
175 die('one or zero DISCNUMBER tags required')
176 }
177 @discnumber = ($tags{DISCNUMBER}->[0]);
178 }
179
180 $artist, $album_tag, $date, \@discnumber
181 }
182
183 sub mangle_for_file_name {
184 my $fn = shift;
185 $fn =~ s/[!,.?]//g; # discard these punctuation marks
186 $fn =~ s/\s+/_/g; # replace all whitespace with _
187 $fn =~ s/[^A-Za-z0-9_]/-/g; # everything else with -
188 $fn =~ s/_[_-]/_/g; # collapse repeated _ or - to _
189 $fn =~ s/-[_-]/_/g; # more
190 $fn =~ s/^[_-]+//; # discard any _ or - at beginning
191 $fn =~ s/[_-]+$//; # at end too
192 $fn
193 }
194
195 sub quote {
196 my $s = shift;
197 $s =~ s/'/'\\''/g;
198 "'$s'"
199 }
200
201 =back
202
203 =cut
204
205 1;