File: linux_cd_parser.py

package info (click to toggle)
exaile 4.2.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 16,452 kB
  • sloc: python: 39,785; javascript: 9,262; makefile: 268; sh: 138
file content (193 lines) | stat: -rw-r--r-- 6,555 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""
This module is a low-level reader and parser for audio CDs.
It heavily relies on ioctls to the linux kernel.

Original source for the code:
http://www.carey.geek.nz/code/python-cdrom/cdtoc.py

Source for all the magical constants and more infos on the ioctls:
linux/include/uapi/linux/cdrom.h
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/cdrom.h
"""

from __future__ import division


import fcntl
import logging
import os
import struct

from xl.trax import Track


logger = logging.getLogger(__name__)


def read_cd_index(device):
    """
    Reads a CD's index (table of contents, TOC).
    This must happen async because the I/O operations may take some time.

    @param device: a path to a CD device
    @return: Array of toc entries. The last one is a dummy. To be read by parse_tracks().
    """
    mcn = None
    toc_entries = []
    fd = os.open(device, os.O_RDONLY)
    try:
        (start, end) = __read_toc_header(fd)
        mcn = __read_disc_mcn(fd)

        # index of the end, i.e. the last toc entry which is an empty dummy
        CDROM_LEADOUT = 0xAA
        for toc_entry_index in list(range(start, end + 1)) + [CDROM_LEADOUT]:
            toc_entry = __read_toc_entry(fd, toc_entry_index)
            # XXX one could also reat+compute the `isrc` track id, see libdiscid
            toc_entries.append(toc_entry)
        logger.debug('Successfully read TOC of CD with MCN %s : %s', mcn, toc_entries)
    except Exception:
        logger.warn('Failed to read CD TOC', exc_info=True)
    finally:
        os.close(fd)
    # clear output for convenience
    if len(toc_entries) == 0:
        toc_entries = None
    return toc_entries, mcn


def __read_toc_header(fd):
    """A wrapper for the `CDROMREADTOCHDR` ioctl"""
    # struct cdrom_tochdr of 2 times u8
    FORMAT_cdrom_tochdr = 'BB'
    # u8 start: lowest track index (index of first track), probably always 1
    # u8 end: highest track index (index of last track), = number of tracks
    cdrom_tochdr = struct.pack(FORMAT_cdrom_tochdr, 0, 0)

    CDROMREADTOCHDR = 0x5305
    cdrom_tochdr = fcntl.ioctl(fd, CDROMREADTOCHDR, cdrom_tochdr)

    start, end = struct.unpack(FORMAT_cdrom_tochdr, cdrom_tochdr)

    return (start, end)


def __read_disc_mcn(fd):
    """A wrapper for the `CDROM_GET_MCN` ioctl"""
    # struct cdrom_mcn of 14 bytes, null-terminated
    FORMAT_cdrom_mcn = '14s'
    cdrom_mcn = struct.pack(FORMAT_cdrom_mcn, b'\0')

    CDROM_GET_MCN = 0x5311
    cdrom_mcn = fcntl.ioctl(fd, CDROM_GET_MCN, cdrom_mcn)

    raw_mcn = struct.unpack(FORMAT_cdrom_mcn, cdrom_mcn)
    mcn = raw_mcn[0][0:13]
    if b'0000000000000' in mcn:
        return None
    else:
        return mcn


def __read_toc_entry(fd, toc_entry_num):
    """A wrapper for the `CDROMREADTOCENTRY` ioctl"""
    # value constant: Minute, Second, Frame: binary (not bcd here)
    CDROM_MSF = 0x02

    # struct cdrom_tocentry of 3 times u8 followed by an int and another u8
    FORMAT_cdrom_tocentry = 'BBBiB'
    # u8 cdte_track: Track number. Starts with 1, which is used for the TOC and contains data.
    # u8 cdte_adr_ctrl: 4 high bits -> cdte_ctrl, 4 low bits -> cdte_adr
    # u8 cdte_format: should be CDROM_MSF=0x02 as requested before
    # int cdte_addr: see below
    # u8 cdte_datamode: ??? (ignored)
    cdrom_tocentry = struct.pack(
        FORMAT_cdrom_tocentry, toc_entry_num, 0, CDROM_MSF, 0, 0
    )

    CDROMREADTOCENTRY = 0x5306
    cdrom_tocentry = fcntl.ioctl(fd, CDROMREADTOCENTRY, cdrom_tocentry)

    cdte_track, cdte_adr_ctrl, cdte_format, cdte_addr, _cdte_datamode = struct.unpack(
        FORMAT_cdrom_tocentry, cdrom_tocentry
    )

    if cdte_format is not CDROM_MSF:
        raise OSError('Invalid syscall answer')

    # unused:
    # cdte_adr = cdte_adr_ctrl & 0x0f  # lower nibble

    cdte_ctrl = (cdte_adr_ctrl & 0xF0) >> 4  # higher nibble

    CDROM_DATA_TRACK = 0x04
    # data: `True` if this "track" contains data, `False` if it is audio
    is_data_track = bool(cdte_ctrl & CDROM_DATA_TRACK)

    # union cdrom_addr of struct cdrom_msf0 and int
    # struct cdrom_msf0 of 3 times u8 plus padding to match size of int
    FORMAT_cdrom_addr = 'BBB' + 'x' * (struct.calcsize('i') - 3)
    # u8 minute: Minutes from beginning of CD
    # u8 second: Seconds after `minute`
    # u8 frame: Frames after `frame`
    minute, second, frame = struct.unpack(
        FORMAT_cdrom_addr, struct.pack('i', cdte_addr)
    )

    return (cdte_track, is_data_track, minute, second, frame)


def parse_tracks(toc_entries, mcn, device):
    """
    Parses the given toc entries and mcn into tracks.
    As a result, the data will only contain track numbers and lengths but
    no sophisticated metadata.

    @param toc_entries: from read_cd_index()
    @param mcn: from read_cd_index()
    @param device: Name of the CD device
    @return: An array of xl.trax.Track with minimal information
    """
    real_track_count = len(toc_entries) - 1  # ignore the empty dummy track at the end
    tracks = []
    for toc_entry_index in range(0, real_track_count):
        (track_index, is_data_track, _, _, _) = toc_entries[toc_entry_index]
        if is_data_track:
            continue
        if track_index is not toc_entry_index + 1:
            logger.warn(
                'Unexpected index found. %ith toc entry claims to be track number %i',
                toc_entry_index,
                track_index,
            )

        length = __calculate_track_length(
            toc_entries[toc_entry_index], toc_entries[toc_entry_index + 1]
        )
        track_uri = "cdda://%d/#%s" % (track_index, device)

        track = Track(uri=track_uri, scan=False)

        track_number = '{0}/{1}'.format(track_index, real_track_count)
        track.set_tags(
            title="Track %d" % track_index, tracknumber=track_number, __length=length
        )

        if mcn:
            track.set_tags(mcn=mcn)

        tracks.append(track)
    return tracks


def __calculate_track_length(current_track, next_track):
    """Calculate length of a single track from its data and the data of the following track"""
    (_, _, begin_minute, begin_second, begin_frame) = current_track
    (_, _, end_minute, end_second, end_frame) = next_track

    length_minutes = end_minute - begin_minute
    length_seconds = end_second - begin_second
    length_frames = end_frame - begin_frame
    # 75 frames per second, see CD_FRAMES in cdrom.h file
    length = length_minutes * 60 + length_seconds + length_frames / 75
    return length