[game-data-packager] 08/21: Add support for ripping CD audio (Closes: #775078)

Simon McVittie smcv at debian.org
Wed Jan 21 11:52:00 UTC 2015


This is an automated email from the git hooks/post-receive script.

smcv pushed a commit to branch master
in repository game-data-packager.

commit 4300389a7ef8106531d27eebaa7ef0257c3863da
Author: Simon McVittie <smcv at debian.org>
Date:   Wed Jan 21 09:31:46 2015 +0000

    Add support for ripping CD audio (Closes: #775078)
---
 lib/game_data_packager/__init__.py | 133 +++++++++++++++++++++++++++++++++++--
 1 file changed, 127 insertions(+), 6 deletions(-)

diff --git a/lib/game_data_packager/__init__.py b/lib/game_data_packager/__init__.py
index 12f7d1c..b7128a2 100644
--- a/lib/game_data_packager/__init__.py
+++ b/lib/game_data_packager/__init__.py
@@ -112,6 +112,9 @@ class DownloadsFailed(Exception):
 class DownloadNotAllowed(Exception):
     pass
 
+class CDRipFailed(Exception):
+    pass
+
 class HashedFile(object):
     def __init__(self, name):
         self.name = name
@@ -353,6 +356,9 @@ class GameDataPackage(object):
         # these names
         self._install_contents_of = set()
 
+        # CD audio stuff from YAML
+        self.rip_cd = {}
+
     @property
     def install(self):
         return self._install
@@ -397,6 +403,7 @@ class GameDataPackage(object):
             'install_to_docdir': self.install_to_docdir,
             'name': self.name,
             'optional': sorted(self.optional),
+            'rip_cd': self.rip_cd,
             'steam': self.steam,
             'symlinks': self.symlinks,
             'type': self.type,
@@ -422,6 +429,9 @@ class GameData(object):
         # binary package name => GameDataPackage
         self.packages = {}
 
+        # Subset of packages.values() with nonempty rip_cd
+        self.rip_cd_packages = set()
+
         # If true, we may compress the .deb. If false, don't.
         self.compress_deb = True
 
@@ -505,6 +515,13 @@ class GameData(object):
         # None or an existing directory in which to save downloaded files.
         self.save_downloads = None
 
+        # Block device from which to rip audio
+        self.cd_device = None
+
+        # Found CD tracks
+        # e.g. { 'quake-music': { 2: '/usr/.../id1/music/track02.ogg' } }
+        self.cd_tracks = {}
+
         self._populate_files(self.yaml.get('files'))
 
         assert 'packages' in self.yaml
@@ -580,10 +597,15 @@ class GameData(object):
                             filename)
                     package.install.add(filename)
 
+            if package.rip_cd:
+                # we only support Ogg Vorbis for now
+                assert package.rip_cd['encoding'] == 'vorbis', package.name
+                self.rip_cd_packages.add(package)
+
         # consistency check
         for package in self.packages.values():
             # there had better be something it wants to install
-            assert package.install, package.name
+            assert package.install or package.rip_cd, package.name
             for installable in package.install:
                 assert installable in self.files, installable
             for installable in package.optional:
@@ -663,7 +685,8 @@ class GameData(object):
 
     def _populate_package(self, package, d):
         for k in ('demo_for', 'expansion_for', 'longname', 'symlinks', 'install_to',
-                'install_to_docdir', 'install_contents_of', 'steam', 'debian'):
+                'install_to_docdir', 'install_contents_of', 'steam', 'debian',
+                'rip_cd'):
             if k in d:
                 setattr(package, k, d[k])
 
@@ -816,6 +839,32 @@ class GameData(object):
         match_path = '/' + path.lower()
         size = os.stat(path).st_size
 
+        for p in self.rip_cd_packages:
+            assert p.rip_cd
+
+            # We use whatever the first track is (usually 2, because track
+            # 1 is data) to locate the rest of the tracks.
+            # We assume tracks in the middle are not missing.
+            look_for = '/' + (p.rip_cd['filename_format'] %
+                    p.rip_cd.get('first_track', 2))
+            if match_path.endswith(look_for):
+                self.cd_tracks[p.name] = {}
+                # make sure it is at least as long as look_for
+                # (corner-case: g-d-p quake id1/music)
+                audio = path
+                if not audio.startswith('/'):
+                    audio = './' + audio
+                basedir = audio[:len(audio) - len(look_for)]
+
+                # The CD audio spec says we can't go beyond track 99.
+                for i in range(p.rip_cd.get('first_track', 2), 100):
+                    audio = os.path.join(basedir,
+                            p.rip_cd['filename_format'] % i)
+                    if not os.path.isfile(audio):
+                        break
+                    self.cd_tracks[p.name][i] = audio
+                return
+
         # if a file (as opposed to a directory) is specified on the
         # command-line, try harder to match it to something
         if really_should_match_something:
@@ -901,15 +950,24 @@ class GameData(object):
 
 
     def consider_file_or_dir(self, path):
-        if os.path.isfile(path):
+        st = os.stat(path)
+
+        if stat.S_ISREG(st.st_mode):
             self.consider_file(path, True)
-        elif os.path.isdir(path):
+        elif stat.S_ISDIR(st.st_mode):
             for dirpath, dirnames, filenames in os.walk(path):
                 for fn in filenames:
                     self.consider_file(os.path.join(dirpath, fn), False)
+        elif stat.S_ISBLK(st.st_mode):
+            if self.rip_cd_packages:
+                self.cd_device = path
+            else:
+                logger.warning('"%s" does not have a package containing CD '
+                        'audio, ignoring block device "%s"',
+                        self.shortname, path)
         else:
-            logger.warning('file "%s" does not exist or is not a file or ' +
-                    'directory', path)
+            logger.warning('file "%s" does not exist or is not a file, ' +
+                    'directory or CD block device', path)
 
     def fill_gaps(self, package, download=False, log=True):
         """Return a FillResult.
@@ -1482,6 +1540,18 @@ class GameData(object):
             mkdir_p(os.path.dirname(os.path.join(destdir, symlink)))
             os.symlink(target, os.path.join(destdir, symlink))
 
+        if package.rip_cd and self.cd_tracks.get(package.name):
+            for i, copy_from in self.cd_tracks[package.name].items():
+                logger.debug('Found CD track %d at %s', i, copy_from)
+                install_to = package.install_to
+                install_as = package.rip_cd['filename_format'] % i
+                copy_to = os.path.join(destdir, install_to, install_as)
+                copy_to_dir = os.path.dirname(copy_to)
+                if not os.path.isdir(copy_to_dir):
+                    mkdir_p(copy_to_dir)
+                subprocess.check_call(['cp', '--reflink=auto', copy_from,
+                    copy_to])
+
         # adapted from dh_md5sums
         subprocess.check_call("find . -type f ! -regex '\./DEBIAN/.*' " +
                 "-printf '%P\\0' | LC_ALL=C sort -z | " +
@@ -1751,6 +1821,9 @@ class GameData(object):
             # we already logged an error
             logger.error('Unable to complete any packages because downloads failed.')
             raise SystemExit(1)
+        except CDRipFailed:
+            logger.error('Unable to rip CD audio')
+            raise SystemExit(1)
 
         if args.destination is None:
             destination = self.get_workdir()
@@ -1781,10 +1854,58 @@ class GameData(object):
         if engines:
             print('it is recommended to also install this game engine: %s' % ', '.join(engines))
 
+    def rip_cd(self, package):
+        cd_device = self.cd_device
+        if cd_device is None:
+            cd_device = '/dev/cdrom'
+
+        logger.info('Ripping CD tracks %d+ from %s for %s',
+                package.rip_cd.get('first_track', 2), cd_device, package.name)
+
+        assert package.rip_cd['encoding'] == 'vorbis', package.name
+        for tool in ('cdparanoia', 'oggenc'):
+            if which(tool) is None:
+                logger.error('cannot rip CD "%s" for package "%s": ' +
+                        '%s is not installed', cd_device, package.name,
+                        tool)
+                raise CDRipFailed()
+
+        mkdir_p(os.path.join(self.get_workdir(), 'tmp'))
+        tmp_wav = os.path.join(self.get_workdir(), 'tmp', 'rip.wav')
+
+        self.cd_tracks[package.name] = {}
+
+        for i in range(package.rip_cd.get('first_track', 2), 100):
+            track = os.path.join(self.get_workdir(), 'tmp', '%d.ogg' % i)
+            if subprocess.call(['cdparanoia', '-d', cd_device, str(i),
+                    tmp_wav]) != 0:
+                break
+            subprocess.check_call(['oggenc', '-o', track, tmp_wav])
+            self.cd_tracks[package.name][i] = track
+            if os.path.exists(tmp_wav):
+                os.remove(tmp_wav)
+
+        if not self.cd_tracks[package.name]:
+            logger.error('Did not rip any CD tracks successfully for "%s"',
+                    package.name)
+            raise CDRipFailed()
+
     def prepare_packages(self, packages, build_demos=False, download=True,
             log_immediately=True):
         possible = set()
 
+        if self.cd_device is not None:
+            rip_cd_packages = self.rip_cd_packages & packages
+            if rip_cd_packages:
+                if len(rip_cd_packages) > 1:
+                    logger.error('cannot rip the same CD for more than one ' +
+                            'music package, please specify one with ' +
+                            '--package: %s',
+                            ', '.join(sorted([p.name
+                                for p in rip_cd_packages])))
+                    raise CDRipFailed()
+                self.rip_cd(rip_cd_packages.pop())
+
         for package in packages:
             if self.fill_gaps(package,
                     log=log_immediately) is not FillResult.IMPOSSIBLE:

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/game-data-packager.git



More information about the Pkg-games-commits mailing list