| 1 |
// ID3 Tag retrieval D module. |
|---|
| 2 |
// written by James Dunne |
|---|
| 3 |
|
|---|
| 4 |
// (c) Copyright James Dunne 2004, 2005 |
|---|
| 5 |
// 06 Dec. 2004 |
|---|
| 6 |
|
|---|
| 7 |
module id3tag; |
|---|
| 8 |
|
|---|
| 9 |
import std.stream; |
|---|
| 10 |
import std.intrinsic; |
|---|
| 11 |
import std.string; |
|---|
| 12 |
|
|---|
| 13 |
// A very simple tag structure, only |
|---|
| 14 |
// concerned with simple song details. |
|---|
| 15 |
struct ID3Tag { |
|---|
| 16 |
// 1 for ID3v1, 2 for ID3v2 |
|---|
| 17 |
int id3version; |
|---|
| 18 |
// The common fields: |
|---|
| 19 |
char[] title; |
|---|
| 20 |
char[] artist; |
|---|
| 21 |
char[] album; |
|---|
| 22 |
int year; |
|---|
| 23 |
int track, tracks; |
|---|
| 24 |
char[] genre; |
|---|
| 25 |
}; |
|---|
| 26 |
|
|---|
| 27 |
// Exception thrown by readID3Tag(): |
|---|
| 28 |
class ID3Exception : Exception { |
|---|
| 29 |
this(char[] msg) { |
|---|
| 30 |
super(msg); |
|---|
| 31 |
} |
|---|
| 32 |
} |
|---|
| 33 |
|
|---|
| 34 |
// Reads an ID3 tag from an MP3 stream. |
|---|
| 35 |
// Stream must be at least both seekable and readable. |
|---|
| 36 |
|
|---|
| 37 |
// This function returns an ID3Tag structure representing the |
|---|
| 38 |
// ID3 information read in. ID3v2 tag is searched for first, |
|---|
| 39 |
// and if not found, ID3v1 is searched for. If neither is found |
|---|
| 40 |
// the function returns 'null'. |
|---|
| 41 |
|
|---|
| 42 |
ID3Tag* readID3Tag(Stream stream) { |
|---|
| 43 |
ID3Tag* tag; |
|---|
| 44 |
|
|---|
| 45 |
// Read a string from a stream: |
|---|
| 46 |
char[] readString(Stream stream, int taglen) { |
|---|
| 47 |
// Read the string: |
|---|
| 48 |
char* str = new char[taglen]; |
|---|
| 49 |
stream.readBlock(cast(void*)str, taglen); |
|---|
| 50 |
|
|---|
| 51 |
return str[0 .. taglen].dup; |
|---|
| 52 |
} |
|---|
| 53 |
|
|---|
| 54 |
// Test stream properties: |
|---|
| 55 |
if (!stream.seekable) throw new ID3Exception("Stream must be seekable"); |
|---|
| 56 |
if (!stream.readable) throw new ID3Exception("Stream must be readable"); |
|---|
| 57 |
|
|---|
| 58 |
char* blk = new char[4]; |
|---|
| 59 |
const char[3] head = "ID3"; |
|---|
| 60 |
|
|---|
| 61 |
// Look for an ID3 v2 tag at the start of the stream: |
|---|
| 62 |
stream.seek(0x00, SeekPos.Set); |
|---|
| 63 |
stream.readBlock(cast(void*)blk, 4); |
|---|
| 64 |
|
|---|
| 65 |
if ((blk[0 .. 3] == head) && (blk[3] == 3)) { |
|---|
| 66 |
// The code below is based totally on my hacking ability, and I know it |
|---|
| 67 |
// to be incorrect for certain ID3v2 layouts. I have yet to hack out |
|---|
| 68 |
// that format. |
|---|
| 69 |
|
|---|
| 70 |
bool done = false; |
|---|
| 71 |
uint taglen; |
|---|
| 72 |
ushort dummy; |
|---|
| 73 |
|
|---|
| 74 |
tag = new ID3Tag; |
|---|
| 75 |
tag.id3version = 2; |
|---|
| 76 |
|
|---|
| 77 |
// Skip 6 bytes: |
|---|
| 78 |
stream.seek(0x06, SeekPos.Current); |
|---|
| 79 |
|
|---|
| 80 |
// Keep reading blocks of 4-character identifiers: |
|---|
| 81 |
while (!done) { |
|---|
| 82 |
// Read the block identifier: |
|---|
| 83 |
stream.readBlock(cast(void*)blk, 4); |
|---|
| 84 |
if (blk[0] == 0) break; |
|---|
| 85 |
|
|---|
| 86 |
// Read the length: |
|---|
| 87 |
stream.readBlock(cast(void*)&taglen, 4); |
|---|
| 88 |
version (LittleEndian) taglen = bswap(taglen); |
|---|
| 89 |
// Read 2 dumb bytes: |
|---|
| 90 |
stream.readBlock(cast(void*)&dummy, 2); |
|---|
| 91 |
// dummy should be byte-swapped also (only as a ushort) |
|---|
| 92 |
// however, it is never used. |
|---|
| 93 |
|
|---|
| 94 |
// Now, what to do with the string? |
|---|
| 95 |
switch (blk[0 .. 4]) { |
|---|
| 96 |
// Song title: |
|---|
| 97 |
case "TIT2": |
|---|
| 98 |
stream.seek(1, SeekPos.Current); --taglen; |
|---|
| 99 |
tag.title = readString(stream, taglen); |
|---|
| 100 |
break; |
|---|
| 101 |
// Song artist: |
|---|
| 102 |
case "TPE1", "TPE2": |
|---|
| 103 |
stream.seek(1, SeekPos.Current); --taglen; |
|---|
| 104 |
tag.artist = readString(stream, taglen); |
|---|
| 105 |
break; |
|---|
| 106 |
// Album title: |
|---|
| 107 |
case "TALB": |
|---|
| 108 |
stream.seek(1, SeekPos.Current); --taglen; |
|---|
| 109 |
tag.album = readString(stream, taglen); |
|---|
| 110 |
break; |
|---|
| 111 |
// Song genre: |
|---|
| 112 |
case "TCON": |
|---|
| 113 |
stream.seek(1, SeekPos.Current); --taglen; |
|---|
| 114 |
tag.genre = readString(stream, taglen); |
|---|
| 115 |
break; |
|---|
| 116 |
// Song track: (can also be "n/N" format where n is track and N is tracks) |
|---|
| 117 |
case "TRCK": |
|---|
| 118 |
stream.seek(1, SeekPos.Current); --taglen; |
|---|
| 119 |
char[][] trkstrs = split(readString(stream, taglen), "/"); |
|---|
| 120 |
tag.track = atoi(trkstrs[0]); |
|---|
| 121 |
// If no / separator, just set a dumb default for maxtracks: |
|---|
| 122 |
if (trkstrs.length == 2) |
|---|
| 123 |
tag.tracks = atoi(trkstrs[1]); |
|---|
| 124 |
else |
|---|
| 125 |
tag.tracks = 0; |
|---|
| 126 |
break; |
|---|
| 127 |
// Album year release: |
|---|
| 128 |
case "TYER": |
|---|
| 129 |
stream.seek(1, SeekPos.Current); --taglen; |
|---|
| 130 |
tag.year = atoi(readString(stream, taglen)); |
|---|
| 131 |
break; |
|---|
| 132 |
|
|---|
| 133 |
// Dummy fields ignored: |
|---|
| 134 |
default: |
|---|
| 135 |
stream.seek(taglen, SeekPos.Current); |
|---|
| 136 |
break; |
|---|
| 137 |
} |
|---|
| 138 |
} |
|---|
| 139 |
return tag; |
|---|
| 140 |
|
|---|
| 141 |
} else { |
|---|
| 142 |
|
|---|
| 143 |
// Look for an ID3 v1 tag 0x80 bytes from end of stream: |
|---|
| 144 |
stream.seek(-0x80, SeekPos.End); |
|---|
| 145 |
stream.readBlock(cast(void*)blk, 3); |
|---|
| 146 |
|
|---|
| 147 |
if (blk[0 .. 3] == "TAG") { |
|---|
| 148 |
char* str = new char[30]; |
|---|
| 149 |
ubyte trk = 0; |
|---|
| 150 |
|
|---|
| 151 |
tag = new ID3Tag; |
|---|
| 152 |
tag.id3version = 1; |
|---|
| 153 |
|
|---|
| 154 |
// All fields are 30 characters long: |
|---|
| 155 |
stream.readBlock(cast(void*)str, 30); |
|---|
| 156 |
tag.title = strip(std.string.toString(str)); |
|---|
| 157 |
|
|---|
| 158 |
str = new char[30]; |
|---|
| 159 |
stream.readBlock(cast(void*)str, 30); |
|---|
| 160 |
tag.artist = strip(std.string.toString(str)); |
|---|
| 161 |
|
|---|
| 162 |
str = new char[30]; |
|---|
| 163 |
stream.readBlock(cast(void*)str, 30); |
|---|
| 164 |
tag.album = strip(std.string.toString(str)); |
|---|
| 165 |
|
|---|
| 166 |
str = new char[4]; |
|---|
| 167 |
stream.readBlock(cast(void*)str, 4); |
|---|
| 168 |
tag.year = atoi(str[0 .. 4]); |
|---|
| 169 |
|
|---|
| 170 |
// I'm not 100% sure, but this seems to be the case: |
|---|
| 171 |
stream.seek(-1, SeekPos.End); |
|---|
| 172 |
stream.readBlock(cast(void*)&trk, 1); |
|---|
| 173 |
tag.track = trk; |
|---|
| 174 |
// No max tracks provided in ID3v1: |
|---|
| 175 |
tag.tracks = 0; |
|---|
| 176 |
|
|---|
| 177 |
// Who knows after this? |
|---|
| 178 |
return tag; |
|---|
| 179 |
|
|---|
| 180 |
} else { |
|---|
| 181 |
|
|---|
| 182 |
// No ID3v1 or ID3v2 tag found. |
|---|
| 183 |
|
|---|
| 184 |
// Please note that I do NOT throw an exception here, |
|---|
| 185 |
// since it is NOT an error that an ID3 tag is not found. |
|---|
| 186 |
// Some files simply do not contain them. Also, there is |
|---|
| 187 |
// the possibility that the stream being scanned is NOT |
|---|
| 188 |
// an MP3 file. |
|---|
| 189 |
|
|---|
| 190 |
return null; |
|---|
| 191 |
|
|---|
| 192 |
} |
|---|
| 193 |
} |
|---|
| 194 |
|
|---|
| 195 |
return null; |
|---|
| 196 |
} |
|---|
| 197 |
|
|---|
| 198 |
// A dummy test: |
|---|
| 199 |
debug { |
|---|
| 200 |
int main(char[][] args) { |
|---|
| 201 |
// Check command arguments: |
|---|
| 202 |
if (args.length < 2) { |
|---|
| 203 |
printf("%.*s <filename.mp3>\n", args[0]); |
|---|
| 204 |
return 1; |
|---|
| 205 |
} |
|---|
| 206 |
|
|---|
| 207 |
// Open the mp3 file: |
|---|
| 208 |
File mp3 = new File(args[1]); |
|---|
| 209 |
|
|---|
| 210 |
// Search for ID3 tag (v2 first, then v1): |
|---|
| 211 |
ID3Tag* tag = readID3Tag(mp3); |
|---|
| 212 |
if (!(tag is null)) { |
|---|
| 213 |
printf("ID3v%d tag\n", tag.id3version); |
|---|
| 214 |
printf("Title: '%.*s'\n", tag.title); |
|---|
| 215 |
printf("Arist: '%.*s'\n", tag.artist); |
|---|
| 216 |
printf("Album: '%.*s'\n", tag.album); |
|---|
| 217 |
printf("Album Year: %d\n", tag.year); |
|---|
| 218 |
printf("Track: %d/%d\n", tag.track, tag.tracks); |
|---|
| 219 |
printf("Genre: '%.*s'\n", tag.genre); |
|---|
| 220 |
} else { |
|---|
| 221 |
printf("No ID3 tag found\n"); |
|---|
| 222 |
} |
|---|
| 223 |
|
|---|
| 224 |
// Close that mp3 file: |
|---|
| 225 |
mp3.close(); |
|---|
| 226 |
|
|---|
| 227 |
// That's it! |
|---|
| 228 |
return 0; |
|---|
| 229 |
} |
|---|
| 230 |
} |
|---|