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