1# -*- coding: utf-8 -*- 2# Copyright (C) 2005 Michael Urman 3# 2006 Lukas Lalinsky 4# 2013 Christoph Reiter 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10 11import errno 12from struct import error as StructError, unpack 13 14from mutagen._util import chr_, text_type 15 16from ._frames import TCON, TRCK, COMM, TDRC, TYER, TALB, TPE1, TIT2 17 18 19def find_id3v1(fileobj, v2_version=4, known_frames=None): 20 """Returns a tuple of (id3tag, offset_to_end) or (None, 0) 21 22 offset mainly because we used to write too short tags in some cases and 23 we need the offset to delete them. 24 25 v2_version: Decides whether ID3v2.3 or ID3v2.4 tags 26 should be returned. Must be 3 or 4. 27 28 known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame 29 IDs to Frame objects 30 """ 31 32 if v2_version not in (3, 4): 33 raise ValueError("Only 3 and 4 possible for v2_version") 34 35 # id3v1 is always at the end (after apev2) 36 37 extra_read = b"APETAGEX".index(b"TAG") 38 39 old_pos = fileobj.tell() 40 try: 41 fileobj.seek(-128 - extra_read, 2) 42 except IOError as e: 43 if e.errno == errno.EINVAL: 44 # If the file is too small, might be ok since we wrote too small 45 # tags at some point. let's see how the parsing goes.. 46 fileobj.seek(0, 0) 47 else: 48 raise 49 50 data = fileobj.read(128 + extra_read) 51 fileobj.seek(old_pos, 0) 52 try: 53 idx = data.index(b"TAG") 54 except ValueError: 55 return (None, 0) 56 else: 57 # FIXME: make use of the apev2 parser here 58 # if TAG is part of APETAGEX assume this is an APEv2 tag 59 try: 60 ape_idx = data.index(b"APETAGEX") 61 except ValueError: 62 pass 63 else: 64 if idx == ape_idx + extra_read: 65 return (None, 0) 66 67 tag = ParseID3v1(data[idx:], v2_version, known_frames) 68 if tag is None: 69 return (None, 0) 70 71 offset = idx - len(data) 72 return (tag, offset) 73 74 75# ID3v1.1 support. 76def ParseID3v1(data, v2_version=4, known_frames=None): 77 """Parse an ID3v1 tag, returning a list of ID3v2 frames 78 79 Returns a {frame_name: frame} dict or None. 80 81 v2_version: Decides whether ID3v2.3 or ID3v2.4 tags 82 should be returned. Must be 3 or 4. 83 84 known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame 85 IDs to Frame objects 86 """ 87 88 if v2_version not in (3, 4): 89 raise ValueError("Only 3 and 4 possible for v2_version") 90 91 try: 92 data = data[data.index(b"TAG"):] 93 except ValueError: 94 return None 95 if 128 < len(data) or len(data) < 124: 96 return None 97 98 # Issue #69 - Previous versions of Mutagen, when encountering 99 # out-of-spec TDRC and TYER frames of less than four characters, 100 # wrote only the characters available - e.g. "1" or "" - into the 101 # year field. To parse those, reduce the size of the year field. 102 # Amazingly, "0s" works as a struct format string. 103 unpack_fmt = "3s30s30s30s%ds29sBB" % (len(data) - 124) 104 105 try: 106 tag, title, artist, album, year, comment, track, genre = unpack( 107 unpack_fmt, data) 108 except StructError: 109 return None 110 111 if tag != b"TAG": 112 return None 113 114 def fix(data): 115 return data.split(b"\x00")[0].strip().decode('latin1') 116 117 title, artist, album, year, comment = map( 118 fix, [title, artist, album, year, comment]) 119 120 frame_class = { 121 "TIT2": TIT2, 122 "TPE1": TPE1, 123 "TALB": TALB, 124 "TYER": TYER, 125 "TDRC": TDRC, 126 "COMM": COMM, 127 "TRCK": TRCK, 128 "TCON": TCON, 129 } 130 for key in frame_class: 131 if known_frames is not None: 132 if key in known_frames: 133 frame_class[key] = known_frames[key] 134 else: 135 frame_class[key] = None 136 137 frames = {} 138 if title and frame_class["TIT2"]: 139 frames["TIT2"] = frame_class["TIT2"](encoding=0, text=title) 140 if artist and frame_class["TPE1"]: 141 frames["TPE1"] = frame_class["TPE1"](encoding=0, text=[artist]) 142 if album and frame_class["TALB"]: 143 frames["TALB"] = frame_class["TALB"](encoding=0, text=album) 144 if year: 145 if v2_version == 3 and frame_class["TYER"]: 146 frames["TYER"] = frame_class["TYER"](encoding=0, text=year) 147 elif frame_class["TDRC"]: 148 frames["TDRC"] = frame_class["TDRC"](encoding=0, text=year) 149 if comment and frame_class["COMM"]: 150 frames["COMM"] = frame_class["COMM"]( 151 encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) 152 153 # Don't read a track number if it looks like the comment was 154 # padded with spaces instead of nulls (thanks, WinAmp). 155 if (track and frame_class["TRCK"] and 156 ((track != 32) or (data[-3] == b'\x00'[0]))): 157 frames["TRCK"] = TRCK(encoding=0, text=str(track)) 158 if genre != 255 and frame_class["TCON"]: 159 frames["TCON"] = TCON(encoding=0, text=str(genre)) 160 return frames 161 162 163def MakeID3v1(id3): 164 """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames.""" 165 166 v1 = {} 167 168 for v2id, name in {"TIT2": "title", "TPE1": "artist", 169 "TALB": "album"}.items(): 170 if v2id in id3: 171 text = id3[v2id].text[0].encode('latin1', 'replace')[:30] 172 else: 173 text = b"" 174 v1[name] = text + (b"\x00" * (30 - len(text))) 175 176 if "COMM" in id3: 177 cmnt = id3["COMM"].text[0].encode('latin1', 'replace')[:28] 178 else: 179 cmnt = b"" 180 v1["comment"] = cmnt + (b"\x00" * (29 - len(cmnt))) 181 182 if "TRCK" in id3: 183 try: 184 v1["track"] = chr_(+id3["TRCK"]) 185 except ValueError: 186 v1["track"] = b"\x00" 187 else: 188 v1["track"] = b"\x00" 189 190 if "TCON" in id3: 191 try: 192 genre = id3["TCON"].genres[0] 193 except IndexError: 194 pass 195 else: 196 if genre in TCON.GENRES: 197 v1["genre"] = chr_(TCON.GENRES.index(genre)) 198 if "genre" not in v1: 199 v1["genre"] = b"\xff" 200 201 if "TDRC" in id3: 202 year = text_type(id3["TDRC"]).encode('ascii') 203 elif "TYER" in id3: 204 year = text_type(id3["TYER"]).encode('ascii') 205 else: 206 year = b"" 207 v1["year"] = (year + b"\x00\x00\x00\x00")[:4] 208 209 return ( 210 b"TAG" + 211 v1["title"] + 212 v1["artist"] + 213 v1["album"] + 214 v1["year"] + 215 v1["comment"] + 216 v1["track"] + 217 v1["genre"] 218 ) 219