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