Compare commits

...
Author SHA1 Message Date
rembo10 16d4ac8895 Fixed some imports in helpers.py 2022-02-09 02:48:50 +05:30
rembo10 f4d60226b3 Disable last.fm getSimilar 2022-02-08 19:37:33 +05:30
rembo10 9ca87e23b2 A less insane library scan interval 2022-02-08 19:27:44 +05:30
rembo10 d934c865c6 Fix for adding albums with no release date 2022-02-08 18:41:24 +05:30
rembo10 de74cd2502 Fix typo in librarysync: track['Location'] 2022-02-08 16:24:11 +05:30
rembo10 f41db714a9 Remove any quotes from str/path values in the config 2022-02-08 16:14:06 +05:30
rembo10 f03b82e5f6 Update mutagen to 1.45.1 2022-02-07 09:30:17 +05:30
rembo10 e2db680b9e Catch NoneType in helpers.py/multikeysort 2022-02-07 08:07:18 +05:30
rembo10 a3db89c11d Update minimum version 2022-02-04 07:42:57 +05:30
rembo10 517d0eb327 Merge branch 'develop' 2022-02-01 20:11:24 +05:30
rembo10 2bacd5a0fc Merge branch 'https' into develop 2022-01-23 14:23:42 +05:30
rembo10 5a559c526d http -> https for album art with small refactor 2022-01-23 14:23:27 +05:30
rembo10 095cee9368 http -> https for internet calls 2022-01-23 14:08:36 +05:30
rembo10 79cb133d1d Remove non-ascii chars in nzb folder name 2022-01-22 08:34:47 +05:30
rembo10 3a9b749017 Merge branch 'develop' 2022-01-22 08:11:01 +05:30
rembo10 0182be2f27 Merge pull request #3284 from hypsometric/py3
Fixes for Python 3.10
2022-01-22 08:01:58 +05:30
hypsometric b6388f7daa Fixes for Python 3.10
- deluge: b64encode returns bytes, transform into string before
  serializing to JSON.
- deluge: no more need to try different encodings for the torrent
  filename or the base64-encoded content, should all be unicode strings
  with Python 3
- collections.abc: Iterable, Mapping, MutableMapping, MutableSequence
  moved from collections to collections.abc since Python 3.3
- pygazelle: html.parser.HTMLParser().unescape() moved to
  html.unescape() in Python 3.4
2022-01-19 18:21:16 +01:00
rembo10 b3199605be v0.5.20 2021-10-15 09:56:59 +05:30
AdeHub 58edc604b3 Merge branch 'develop' 2021-07-08 19:57:08 +12:00
AdeHub 379fd3d0b8 Merge branch 'develop' 2020-10-17 11:06:38 +13:00
AdeHub bf74f57535 Merge branch 'develop' 2020-05-28 10:49:51 +12:00
AdeHub f18334d87c Merge branch 'develop' 2020-03-07 10:30:57 +13:00
AdeHub 5283b48736 Merge branch 'develop' 2019-09-08 17:38:17 +12:00
Ade dc22bb006d Hotfix index creation from develop
Fixes #3175
2019-01-08 21:09:41 +13:00
91 changed files with 3874 additions and 2089 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ import os
import sys import sys
if sys.version_info <= (3, 5): if sys.version_info <= (3, 5):
sys.stdout.write("Headphones requires Python >= 3.5\n") sys.stdout.write("Headphones requires Python >= 3.6\n")
sys.exit(1) sys.exit(1)
# Ensure lib added to path, before any other imports # Ensure lib added to path, before any other imports
+15 -8
View File
@@ -28,7 +28,7 @@ def getAlbumArt(albumid):
# CAA # CAA
logger.info("Searching for artwork at CAA") logger.info("Searching for artwork at CAA")
artwork_path = 'http://coverartarchive.org/release-group/%s/front' % albumid artwork_path = 'https://coverartarchive.org/release-group/%s/front' % albumid
artwork = getartwork(artwork_path) artwork = getartwork(artwork_path)
if artwork: if artwork:
logger.info("Artwork found at CAA") logger.info("Artwork found at CAA")
@@ -41,7 +41,7 @@ def getAlbumArt(albumid):
'SELECT ArtistName, AlbumTitle, ReleaseID, AlbumASIN FROM albums WHERE AlbumID=?', 'SELECT ArtistName, AlbumTitle, ReleaseID, AlbumASIN FROM albums WHERE AlbumID=?',
[albumid]).fetchone() [albumid]).fetchone()
if dbalbum['AlbumASIN']: if dbalbum['AlbumASIN']:
artwork_path = 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % dbalbum['AlbumASIN'] artwork_path = 'https://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % dbalbum['AlbumASIN']
artwork = getartwork(artwork_path) artwork = getartwork(artwork_path)
if artwork: if artwork:
logger.info("Artwork found at Amazon") logger.info("Artwork found at Amazon")
@@ -156,12 +156,19 @@ def getartwork(artwork_path):
break break
elif maxwidth and img_width > maxwidth: elif maxwidth and img_width > maxwidth:
# Downsize using proxy service to max width # Downsize using proxy service to max width
artwork_path = '{0}?{1}'.format('http://images.weserv.nl/', urlencode({
'url': artwork_path.replace('http://', ''),
'w': maxwidth,
}))
artwork = bytes() artwork = bytes()
r = request.request_response(artwork_path, timeout=20, stream=True, whitelist_status_code=404) url = "https://images.weserv.nl"
params = {
"url": artwork_path,
"w": maxwidth
}
r = request.request_response(
url,
params=params,
timeout=20,
stream=True,
whitelist_status_code=404
)
if r: if r:
for chunk in r.iter_content(chunk_size=1024): for chunk in r.iter_content(chunk_size=1024):
artwork += chunk artwork += chunk
@@ -182,7 +189,7 @@ def getCachedArt(albumid):
if not artwork_path: if not artwork_path:
return return
if artwork_path.startswith('http://'): if artwork_path.startswith("http"):
artwork = request.request_content(artwork_path, timeout=20) artwork = request.request_content(artwork_path, timeout=20)
if not artwork: if not artwork:
+11 -6
View File
@@ -540,12 +540,17 @@ class Cache(object):
artwork_thumb = None artwork_thumb = None
if 'fanart' in thumb_url: if 'fanart' in thumb_url:
# Create thumb using image resizing service # Create thumb using image resizing service
artwork_path = '{0}?{1}'.format('http://images.weserv.nl/', urlencode({ url = "https://images.weserv.nl"
'url': thumb_url.replace('http://', ''), params = {
'w': 300, "url": thumb_url,
})) "w": 300
artwork_thumb = request.request_content(artwork_path, timeout=20, whitelist_status_code=404) }
artwork_thumb = request.request_content(
url,
params=params,
timeout=20,
whitelist_status_code=404
)
if artwork_thumb: if artwork_thumb:
with open(thumb_path, 'wb') as f: with open(thumb_path, 'wb') as f:
f.write(artwork_thumb) f.write(artwork_thumb)
+5 -5
View File
@@ -157,7 +157,7 @@ _CONFIG_DEFINITIONS = {
'LASTFM_USERNAME': (str, 'General', ''), 'LASTFM_USERNAME': (str, 'General', ''),
'LAUNCH_BROWSER': (int, 'General', 1), 'LAUNCH_BROWSER': (int, 'General', 1),
'LIBRARYSCAN': (int, 'General', 1), 'LIBRARYSCAN': (int, 'General', 1),
'LIBRARYSCAN_INTERVAL': (int, 'General', 300), 'LIBRARYSCAN_INTERVAL': (int, 'General', 24),
'LMS_ENABLED': (int, 'LMS', 0), 'LMS_ENABLED': (int, 'LMS', 0),
'LMS_HOST': (str, 'LMS', ''), 'LMS_HOST': (str, 'LMS', ''),
'LOG_DIR': (path, 'General', ''), 'LOG_DIR': (path, 'General', ''),
@@ -363,12 +363,12 @@ class Config(object):
try: try:
my_val = definition_type(self._config[section][ini_key]) my_val = definition_type(self._config[section][ini_key])
# ConfigParser interprets empty strings in the config # ConfigParser interprets quotes in the config
# literally, so we need to sanitize it. It's not really # literally, so we need to sanitize it. It's not really
# a config upgrade, since a user can at any time put # a config upgrade, since a user can at any time put
# some_key = '' # some_key = 'some_val'
if my_val == '""' or my_val == "''": if type(my_val) in [str, path]:
my_val = '' my_val = my_val.strip('"').strip("'")
except Exception: except Exception:
my_val = default my_val = default
self._config[section][ini_key] = str(my_val) self._config[section][ini_key] = str(my_val)
+1 -20
View File
@@ -472,32 +472,13 @@ def _add_torrent_file(result):
# content is torrent file contents that needs to be encoded to base64 # content is torrent file contents that needs to be encoded to base64
post_data = json.dumps({"method": "core.add_torrent_file", post_data = json.dumps({"method": "core.add_torrent_file",
"params": [result['name'] + '.torrent', "params": [result['name'] + '.torrent',
b64encode(result['content'].encode('utf8')), {}], b64encode(result['content']).decode(), {}],
"id": 2}) "id": 2})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert, headers=headers) verify=deluge_verify_cert, headers=headers)
result['hash'] = json.loads(response.text)['result'] result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text))) logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result'] return json.loads(response.text)['result']
except UnicodeDecodeError:
try:
# content is torrent file contents that needs to be encoded to base64
# this time let's try leaving the encoding as is
logger.debug('Deluge: There was a decoding issue, let\'s try again')
post_data = json.dumps({"method": "core.add_torrent_file",
"params": [result['name'].decode('utf8') + '.torrent',
b64encode(result['content']), {}],
"id": 22})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert, headers=headers)
result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result']
except Exception as e:
logger.error('Deluge: Adding torrent file failed after decode: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
logger.error('; '.join(formatted_lines))
return False
except Exception as e: except Exception as e:
logger.error('Deluge: Adding torrent file failed: %s' % str(e)) logger.error('Deluge: Adding torrent file failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines() formatted_lines = traceback.format_exc().splitlines()
+36 -29
View File
@@ -14,24 +14,25 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from operator import itemgetter import os
import unicodedata import re
import datetime
import shutil import shutil
import time
import sys import sys
import tempfile import tempfile
import glob import time
import unicodedata
from contextlib import contextmanager
from datetime import datetime, date
from fnmatch import fnmatch
from functools import cmp_to_key
from glob import glob
from operator import itemgetter
from beets import logging as beetslogging from beets import logging as beetslogging
import six
from contextlib import contextmanager
import fnmatch
import functools
import re
import os
from mediafile import MediaFile, FileTypeError, UnreadableFileError from mediafile import MediaFile, FileTypeError, UnreadableFileError
from six import text_type
from unidecode import unidecode
import headphones import headphones
@@ -51,6 +52,13 @@ def cmp(x, y):
https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function
""" """
if x is None and y is None:
return 0
elif x is None:
return -1
elif y is None:
return 1
else:
return (x > y) - (x < y) return (x > y) - (x < y)
def multikeysort(items, columns): def multikeysort(items, columns):
@@ -66,7 +74,7 @@ def multikeysort(items, columns):
else: else:
return 0 return 0
return sorted(items, key=functools.cmp_to_key(comparer)) return sorted(items, key=cmp_to_key(comparer))
def checked(variable): def checked(variable):
@@ -148,28 +156,25 @@ def convert_seconds(s):
def today(): def today():
today = datetime.date.today() return date.isoformat(date.today())
yyyymmdd = datetime.date.isoformat(today)
return yyyymmdd
def now(): def now():
now = datetime.datetime.now() now = datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S") return now.strftime("%Y-%m-%d %H:%M:%S")
def get_age(date): def is_valid_date(d):
try: if not d:
split_date = date.split('-')
except:
return False return False
else:
return bool(re.match(r'\d{4}-\d{2}-\d{2}', d))
try:
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
except (IndexError, ValueError):
days_old = False
return days_old def age(d):
'''Requires a valid date'''
delta = date.today() - date.fromisoformat(d)
return delta.days
def bytes_to_mb(bytes): def bytes_to_mb(bytes):
@@ -501,7 +506,7 @@ def path_match_patterns(path, patterns):
""" """
for pattern in patterns: for pattern in patterns:
if fnmatch.fnmatch(path, pattern): if fnmatch(path, pattern):
return True return True
# No match # No match
@@ -707,7 +712,7 @@ def preserve_torrent_directory(albumpath, forced=False, single=False):
workdir = os.path.join(tempdir, prefix) workdir = os.path.join(tempdir, prefix)
workdir = re.sub(r'\[', '[[]', workdir) workdir = re.sub(r'\[', '[[]', workdir)
workdir = re.sub(r'(?<!\[)\]', '[]]', workdir) workdir = re.sub(r'(?<!\[)\]', '[]]', workdir)
if len(glob.glob(workdir + '*/')) >= 3: if len(glob(workdir + '*/')) >= 3:
logger.error( logger.error(
"Looks like a temp directory has previously been created " "Looks like a temp directory has previously been created "
"for this albumpath, not continuing " "for this albumpath, not continuing "
@@ -952,6 +957,8 @@ def sab_sanitize_foldername(name):
if not name: if not name:
return return
name = unidecode(name)
lst = [] lst = []
for ch in name.strip(): for ch in name.strip():
if ch in FL_ILLEGAL: if ch in FL_ILLEGAL:
@@ -1024,7 +1031,7 @@ class BeetsLogCapture(beetslogging.Handler):
self.messages = [] self.messages = []
def emit(self, record): def emit(self, record):
self.messages.append(six.text_type(record.msg)) self.messages.append(text_type(record.msg))
@contextmanager @contextmanager
+11 -1
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from .unittestcompat import TestCase from .unittestcompat import TestCase
from headphones.helpers import clean_name from headphones.helpers import clean_name, is_valid_date, age
class HelpersTest(TestCase): class HelpersTest(TestCase):
@@ -46,3 +46,13 @@ class HelpersTest(TestCase):
self.assertEqual( self.assertEqual(
test, expected, "check clean_name() with narrow non-ascii input" test, expected, "check clean_name() with narrow non-ascii input"
) )
def test_is_valid_date(date):
test_cases = [
('2021-11-12', True, "check is_valid_date returns True for valid date"),
(None, False, "check is_valid_date returns False for None"),
('2021-11', False, "check is_valid_date returns False for incomplete"),
('2021', False, "check is_valid_date returns False for incomplete")
]
for input, expected, desc in test_cases:
self.assertEqual(is_valid_date(input), expected, desc)
+6 -11
View File
@@ -245,7 +245,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
rgid = rg['id'] rgid = rg['id']
skip_log = 0 skip_log = 0
# Make a user configurable variable to skip update of albums with release dates older than this date (in days) # Make a user configurable variable to skip update of albums with release dates older than this date (in days)
pause_delta = headphones.CONFIG.MB_IGNORE_AGE ignore_age = headphones.CONFIG.MB_IGNORE_AGE
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone() rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
@@ -279,13 +279,13 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
release_date = check_release_date + "-12-31" release_date = check_release_date + "-12-31"
else: else:
release_date = today release_date = today
if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta: if helpers.age(release_date) < ignore_age:
logger.info("[%s] Now updating: %s (Release Date <%s Days)", logger.info("[%s] Now updating: %s (Release Date <%s Days)",
artist['artist_name'], rg['title'], pause_delta) artist['artist_name'], rg['title'], ignore_age)
new_releases = mb.get_new_releases(rgid, includeExtras, True) new_releases = mb.get_new_releases(rgid, includeExtras, True)
else: else:
logger.info("[%s] Skipping: %s (Release Date >%s Days)", logger.info("[%s] Skipping: %s (Release Date >%s Days)",
artist['artist_name'], rg['title'], pause_delta) artist['artist_name'], rg['title'], ignore_age)
skip_log = 1 skip_log = 1
new_releases = 0 new_releases = 0
@@ -450,13 +450,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
if headphones.CONFIG.AUTOWANT_ALL: if headphones.CONFIG.AUTOWANT_ALL:
newValueDict['Status'] = "Wanted" newValueDict['Status'] = "Wanted"
elif album['ReleaseDate'] > today and headphones.CONFIG.AUTOWANT_UPCOMING: elif headphones.CONFIG.AUTOWANT_UPCOMING:
newValueDict['Status'] = "Wanted" if helpers.is_valid_date(album['ReleaseDate']) and helpers.age(album['ReleaseDate']) < 21:
# Sometimes "new" albums are added to musicbrainz after their release date, so let's try to catch these
# The first test just makes sure we have year-month-day
elif helpers.get_age(album['ReleaseDate']) and helpers.get_age(
today) - helpers.get_age(
album['ReleaseDate']) < 21 and headphones.CONFIG.AUTOWANT_UPCOMING:
newValueDict['Status'] = "Wanted" newValueDict['Status'] = "Wanted"
else: else:
newValueDict['Status'] = "Skipped" newValueDict['Status'] = "Skipped"
+3 -3
View File
@@ -22,7 +22,7 @@ from headphones import db, logger, request
TIMEOUT = 60.0 # seconds TIMEOUT = 60.0 # seconds
REQUEST_LIMIT = 1.0 / 5 # seconds REQUEST_LIMIT = 1.0 / 5 # seconds
ENTRY_POINT = "http://ws.audioscrobbler.com/2.0/" ENTRY_POINT = "https://ws.audioscrobbler.com/2.0/"
API_KEY = "395e6ec6bb557382fc41fde867bce66f" API_KEY = "395e6ec6bb557382fc41fde867bce66f"
# Required for API request limit # Required for API request limit
@@ -63,12 +63,12 @@ def request_lastfm(method, **kwargs):
def getSimilar(): def getSimilar():
myDB = db.DBConnection() myDB = db.DBConnection()
results = myDB.select("SELECT ArtistID from artists ORDER BY HaveTracks DESC") results = myDB.select("SELECT ArtistID from artists ORDER BY HaveTracks DESC LIMIT 10")
logger.info("Fetching similar artists from Last.FM for tag cloud") logger.info("Fetching similar artists from Last.FM for tag cloud")
artistlist = [] artistlist = []
for result in results[:12]: for result in results:
data = request_lastfm("artist.getsimilar", mbid=result["ArtistId"]) data = request_lastfm("artist.getsimilar", mbid=result["ArtistId"])
if data and "similarartists" in data: if data and "similarartists" in data:
+5 -4
View File
@@ -77,9 +77,9 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
if track['ArtistName']: if track['ArtistName']:
# Make sure deleted files get accounted for when updating artist track counts # Make sure deleted files get accounted for when updating artist track counts
new_artists.append(track['ArtistName']) new_artists.append(track['ArtistName'])
myDB.action('DELETE FROM have WHERE Location=?', [Track['Location']]) myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
logger.info( logger.info(
f"{Track['Location']} removed from Headphones, as it " f"{track['Location']} removed from Headphones, as it "
f"is no longer on disk" f"is no longer on disk"
) )
@@ -442,8 +442,9 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# if not append: # if not append:
# update_album_status() # update_album_status()
if not append and not artistScan: # TODO: Fix last.fm api calls
lastfm.getSimilar() #if not append and not artistScan:
#lastfm.getSimilar()
if ArtistName: if ArtistName:
logger.info('Scanning complete for artist: %s', ArtistName) logger.info('Scanning complete for artist: %s', ArtistName)
+1 -1
View File
@@ -25,7 +25,7 @@ def getLyrics(artist, song):
"fmt": 'xml' "fmt": 'xml'
} }
url = 'http://lyrics.wikia.com/api.php' url = 'https://lyrics.wikia.com/api.php'
data = request.request_minidom(url, params=params) data = request.request_minidom(url, params=params)
if not data: if not data:
+7 -19
View File
@@ -124,11 +124,7 @@ def findArtist(name, limit=1):
'Cannot determine the best match from an artist/album search. Using top match instead') 'Cannot determine the best match from an artist/album search. Using top match instead')
artistlist.append({ artistlist.append({
# Just need the artist id if the limit is 1 # Just need the artist id if the limit is 1
# 'name': unicode(result['sort-name']),
# 'uniquename': uniquename,
'id': str(result['id']), 'id': str(result['id']),
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
# 'score': int(result['ext:score'])
}) })
else: else:
artistlist.append(artistdict) artistlist.append(artistdict)
@@ -137,7 +133,7 @@ def findArtist(name, limit=1):
'name': str(result['sort-name']), 'name': str(result['sort-name']),
'uniquename': uniquename, 'uniquename': uniquename,
'id': str(result['id']), 'id': str(result['id']),
'url': str("http://musicbrainz.org/artist/" + result['id']), 'url': str("https://musicbrainz.org/artist/" + result['id']),
# probably needs to be changed # probably needs to be changed
'score': int(result['ext:score']) 'score': int(result['ext:score'])
}) })
@@ -208,9 +204,9 @@ def findRelease(name, limit=1, artist=None):
'id': str(result['artist-credit'][0]['artist']['id']), 'id': str(result['artist-credit'][0]['artist']['id']),
'albumid': str(result['id']), 'albumid': str(result['id']),
'url': str( 'url': str(
"http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']), "https://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),
# probably needs to be changed # probably needs to be changed
'albumurl': str("http://musicbrainz.org/release/" + result['id']), 'albumurl': str("https://musicbrainz.org/release/" + result['id']),
# probably needs to be changed # probably needs to be changed
'score': int(result['ext:score']), 'score': int(result['ext:score']),
'date': str(result['date']) if 'date' in result else '', 'date': str(result['date']) if 'date' in result else '',
@@ -248,7 +244,7 @@ def findSeries(name, limit=1):
'name': str(result['name']), 'name': str(result['name']),
'type': str(result['type']), 'type': str(result['type']),
'id': str(result['id']), 'id': str(result['id']),
'url': str("http://musicbrainz.org/series/" + result['id']), 'url': str("https://musicbrainz.org/series/" + result['id']),
# probably needs to be changed # probably needs to be changed
'score': int(result['ext:score']) 'score': int(result['ext:score'])
}) })
@@ -295,7 +291,7 @@ def getArtist(artistid, extrasonly=False):
releasegroups.append({ releasegroups.append({
'title': str(rg['title']), 'title': str(rg['title']),
'id': str(rg['id']), 'id': str(rg['id']),
'url': "http://musicbrainz.org/release-group/" + rg['id'], 'url': "https://musicbrainz.org/release-group/" + rg['id'],
'type': str(rg['type']) 'type': str(rg['type'])
}) })
@@ -356,7 +352,7 @@ def getArtist(artistid, extrasonly=False):
releasegroups.append({ releasegroups.append({
'title': str(rg['title']), 'title': str(rg['title']),
'id': str(rg['id']), 'id': str(rg['id']),
'url': "http://musicbrainz.org/release-group/" + rg['id'], 'url': "https://musicbrainz.org/release-group/" + rg['id'],
'type': str(rg_type) 'type': str(rg_type)
}) })
artist_dict['releasegroups'] = releasegroups artist_dict['releasegroups'] = releasegroups
@@ -691,7 +687,7 @@ def getTracksFromRelease(release):
'number': totalTracks, 'number': totalTracks,
'title': track_title, 'title': track_title,
'id': str(track['recording']['id']), 'id': str(track['recording']['id']),
'url': "http://musicbrainz.org/track/" + track['recording']['id'], 'url': "https://musicbrainz.org/track/" + track['recording']['id'],
'duration': int(track['length']) if 'length' in track else 0 'duration': int(track['length']) if 'length' in track else 0
}) })
totalTracks += 1 totalTracks += 1
@@ -733,15 +729,7 @@ def findArtistbyAlbum(name):
for releaseGroup in results: for releaseGroup in results:
newArtist = releaseGroup['artist-credit'][0]['artist'] newArtist = releaseGroup['artist-credit'][0]['artist']
# Only need the artist ID if we're doing an artist+album lookup # Only need the artist ID if we're doing an artist+album lookup
# if 'disambiguation' in newArtist:
# uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")")
# else:
# uniquename = unicode(newArtist['sort-name'])
# artist_dict['name'] = unicode(newArtist['sort-name'])
# artist_dict['uniquename'] = uniquename
artist_dict['id'] = str(newArtist['id']) artist_dict['id'] = str(newArtist['id'])
# artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id']
# artist_dict['score'] = int(releaseGroup['ext:score'])
return artist_dict return artist_dict
+2 -2
View File
@@ -920,7 +920,7 @@ class BOXCAR(object):
def notify(self, title, message, rgid=None): def notify(self, title, message, rgid=None):
try: try:
if rgid: if rgid:
message += '<br></br><a href="http://musicbrainz.org/' \ message += '<br></br><a href="https://musicbrainz.org/' \
'release-group/%s">MusicBrainz</a>' % rgid 'release-group/%s">MusicBrainz</a>' % rgid
data = urllib.parse.urlencode({ data = urllib.parse.urlencode({
@@ -1019,7 +1019,7 @@ class TELEGRAM(object):
# MusicBrainz link # MusicBrainz link
if rgid: if rgid:
message += '\n\n <a href="http://musicbrainz.org/' \ message += '\n\n <a href="https://musicbrainz.org/' \
'release-group/%s">MusicBrainz</a>' % rgid 'release-group/%s">MusicBrainz</a>' % rgid
# Send image # Send image
+5 -5
View File
@@ -19,13 +19,13 @@ class Rutracker(object):
self.timeout = 60 self.timeout = 60
self.loggedin = False self.loggedin = False
self.maxsize = 0 self.maxsize = 0
self.search_referer = 'http://rutracker.org/forum/tracker.php' self.search_referer = 'https://rutracker.org/forum/tracker.php'
def logged_in(self): def logged_in(self):
return self.loggedin return self.loggedin
def still_logged_in(self, html): def still_logged_in(self, html):
if not html or "action=\"http://rutracker.org/forum/login.php\">" in html: if not html or "action=\"https://rutracker.org/forum/login.php\">" in html:
return False return False
else: else:
return True return True
@@ -35,7 +35,7 @@ class Rutracker(object):
Logs in user Logs in user
""" """
loginpage = 'http://rutracker.org/forum/login.php' loginpage = 'https://rutracker.org/forum/login.php'
post_params = { post_params = {
'login_username': headphones.CONFIG.RUTRACKER_USER, 'login_username': headphones.CONFIG.RUTRACKER_USER,
'login_password': headphones.CONFIG.RUTRACKER_PASSWORD, 'login_password': headphones.CONFIG.RUTRACKER_PASSWORD,
@@ -159,7 +159,7 @@ class Rutracker(object):
# Torrent topic page # Torrent topic page
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])[ torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])[
't'] 't']
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id topicurl = 'https://rutracker.org/forum/viewtopic.php?t=' + torrent_id
rulist.append((title, size, topicurl, 'rutracker.org', 'torrent', True)) rulist.append((title, size, topicurl, 'rutracker.org', 'torrent', True))
else: else:
logger.info("%s is larger than the maxsize or has too little seeders for this category, " logger.info("%s is larger than the maxsize or has too little seeders for this category, "
@@ -179,7 +179,7 @@ class Rutracker(object):
return the .torrent data return the .torrent data
""" """
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t'] torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
downloadurl = 'http://rutracker.org/forum/dl.php?t=' + torrent_id downloadurl = 'https://rutracker.org/forum/dl.php?t=' + torrent_id
cookie = {'bb_dl': torrent_id} cookie = {'bb_dl': torrent_id}
try: try:
headers = {'Referer': url} headers = {'Referer': url}
+6 -6
View File
@@ -40,7 +40,7 @@ from bencode import decode as bdecode
# Magnet to torrent services, for Black hole. Stolen from CouchPotato. # Magnet to torrent services, for Black hole. Stolen from CouchPotato.
TORRENT_TO_MAGNET_SERVICES = [ TORRENT_TO_MAGNET_SERVICES = [
'http://itorrents.org/torrent/%s.torrent', 'https://itorrents.org/torrent/%s.torrent',
'https://cache.torrentgalaxy.org/get/%s', 'https://cache.torrentgalaxy.org/get/%s',
'https://www.seedpeer.me/torrent/%s' 'https://www.seedpeer.me/torrent/%s'
] ]
@@ -611,7 +611,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
provider = newznab_host[0] provider = newznab_host[0]
# Add a little mod for kere.ws # Add a little mod for kere.ws
if newznab_host[0] == "http://kere.ws": if newznab_host[0] == "https://kere.ws":
if categories == "3040": if categories == "3040":
categories = categories + ",4070" categories = categories + ",4070"
elif categories == "3040,3010": elif categories == "3040,3010":
@@ -682,7 +682,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
} }
data = request.request_feed( data = request.request_feed(
url='http://beta.nzbs.org/api', url='https://beta.nzbs.org/api',
params=params, headers=headers, params=params, headers=headers,
timeout=5 timeout=5
) )
@@ -731,7 +731,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
} }
data = request.request_json( data = request.request_json(
url='http://api.omgwtfnzbs.me/json/', url='https://api.omgwtfnzbs.me/json/',
params=params, headers=headers params=params, headers=headers
) )
@@ -1261,7 +1261,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
def set_proxy(proxy_url): def set_proxy(proxy_url):
if not proxy_url.startswith('http'): if not proxy_url.startswith('http'):
proxy_url = 'http://' + proxy_url proxy_url = 'https://' + proxy_url
if proxy_url.endswith('/'): if proxy_url.endswith('/'):
proxy_url = proxy_url[:-1] proxy_url = proxy_url[:-1]
@@ -1467,7 +1467,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
if headphones.CONFIG.ORPHEUS: if headphones.CONFIG.ORPHEUS:
provider = "Orpheus.network" provider = "Orpheus.network"
providerurl = "http://orpheus.network/" providerurl = "https://orpheus.network/"
bitrate = None bitrate = None
bitrate_string = bitrate bitrate_string = bitrate
+4 -4
View File
@@ -1677,16 +1677,16 @@ class WebInterface(object):
# Return the Cover Art Archive urls if not found on last.fm # Return the Cover Art Archive urls if not found on last.fm
if AlbumID and not image_dict: if AlbumID and not image_dict:
image_url = "http://coverartarchive.org/release/%s/front-500.jpg" % AlbumID image_url = "https://coverartarchive.org/release/%s/front-500.jpg" % AlbumID
thumb_url = "http://coverartarchive.org/release/%s/front-250.jpg" % AlbumID thumb_url = "https://coverartarchive.org/release/%s/front-250.jpg" % AlbumID
image_dict = {'artwork': image_url, 'thumbnail': thumb_url} image_dict = {'artwork': image_url, 'thumbnail': thumb_url}
elif AlbumID and (not image_dict['artwork'] or not image_dict['thumbnail']): elif AlbumID and (not image_dict['artwork'] or not image_dict['thumbnail']):
if not image_dict['artwork']: if not image_dict['artwork']:
image_dict[ image_dict[
'artwork'] = "http://coverartarchive.org/release/%s/front-500.jpg" % AlbumID 'artwork'] = "https://coverartarchive.org/release/%s/front-500.jpg" % AlbumID
if not image_dict['thumbnail']: if not image_dict['thumbnail']:
image_dict[ image_dict[
'thumbnail'] = "http://coverartarchive.org/release/%s/front-250.jpg" % AlbumID 'thumbnail'] = "https://coverartarchive.org/release/%s/front-250.jpg" % AlbumID
return image_dict return image_dict
+1 -1
View File
@@ -1,4 +1,4 @@
from collections import Iterable, Mapping from collections.abc import Iterable, Mapping
from uuid import uuid4 from uuid import uuid4
import six import six
+1 -1
View File
@@ -1,6 +1,6 @@
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from collections import MutableMapping from collections.abc import MutableMapping
from threading import RLock from threading import RLock
from datetime import datetime from datetime import datetime
from logging import getLogger from logging import getLogger
Executable → Regular
+1 -1
View File
@@ -23,7 +23,7 @@ from mutagen._util import MutagenError
from mutagen._file import FileType, StreamInfo, File from mutagen._file import FileType, StreamInfo, File
from mutagen._tags import Tags, Metadata, PaddingInfo from mutagen._tags import Tags, Metadata, PaddingInfo
version = (1, 38, -1) version = (1, 45, 1)
"""Version tuple.""" """Version tuple."""
version_string = ".".join(map(str, version)) version_string = ".".join(map(str, version))
-86
View File
@@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2013 Christoph Reiter
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
import sys
PY2 = sys.version_info[0] == 2
PY3 = not PY2
if PY2:
from io import StringIO
BytesIO = StringIO
from io import StringIO as cBytesIO
long_ = int
integer_types = (int, int)
string_types = (str, str)
text_type = str
xrange = xrange
cmp = cmp
chr_ = chr
def endswith(text, end):
return text.endswith(end)
iteritems = lambda d: iter(d.items())
itervalues = lambda d: iter(d.values())
iterkeys = lambda d: iter(d.keys())
iterbytes = lambda b: iter(b)
exec("def reraise(tp, value, tb):\n raise tp, value, tb")
def swap_to_string(cls):
if "__str__" in cls.__dict__:
cls.__unicode__ = cls.__str__
if "__bytes__" in cls.__dict__:
cls.__str__ = cls.__bytes__
return cls
elif PY3:
from io import StringIO
StringIO = StringIO
from io import BytesIO
cBytesIO = BytesIO
long_ = int
integer_types = (int,)
string_types = (str,)
text_type = str
izip = zip
xrange = range
cmp = lambda a, b: (a > b) - (a < b)
chr_ = lambda x: bytes([x])
def endswith(text, end):
# usefull for paths which can be both, str and bytes
if isinstance(text, str):
if not isinstance(end, str):
end = end.decode("ascii")
else:
if not isinstance(end, bytes):
end = end.encode("ascii")
return text.endswith(end)
iteritems = lambda d: iter(list(d.items()))
itervalues = lambda d: iter(list(d.values()))
iterkeys = lambda d: iter(list(d.keys()))
iterbytes = lambda b: (bytes([v]) for v in b)
def reraise(tp, value, tb):
raise tp(value).with_traceback(tb)
def swap_to_string(cls):
return cls
Executable → Regular
+192 -192
View File
@@ -8,197 +8,197 @@
"""Constants used by Mutagen.""" """Constants used by Mutagen."""
GENRES = [ GENRES = [
"Blues", u"Blues",
"Classic Rock", u"Classic Rock",
"Country", u"Country",
"Dance", u"Dance",
"Disco", u"Disco",
"Funk", u"Funk",
"Grunge", u"Grunge",
"Hip-Hop", u"Hip-Hop",
"Jazz", u"Jazz",
"Metal", u"Metal",
"New Age", u"New Age",
"Oldies", u"Oldies",
"Other", u"Other",
"Pop", u"Pop",
"R&B", u"R&B",
"Rap", u"Rap",
"Reggae", u"Reggae",
"Rock", u"Rock",
"Techno", u"Techno",
"Industrial", u"Industrial",
"Alternative", u"Alternative",
"Ska", u"Ska",
"Death Metal", u"Death Metal",
"Pranks", u"Pranks",
"Soundtrack", u"Soundtrack",
"Euro-Techno", u"Euro-Techno",
"Ambient", u"Ambient",
"Trip-Hop", u"Trip-Hop",
"Vocal", u"Vocal",
"Jazz+Funk", u"Jazz+Funk",
"Fusion", u"Fusion",
"Trance", u"Trance",
"Classical", u"Classical",
"Instrumental", u"Instrumental",
"Acid", u"Acid",
"House", u"House",
"Game", u"Game",
"Sound Clip", u"Sound Clip",
"Gospel", u"Gospel",
"Noise", u"Noise",
"Alt. Rock", u"Alt. Rock",
"Bass", u"Bass",
"Soul", u"Soul",
"Punk", u"Punk",
"Space", u"Space",
"Meditative", u"Meditative",
"Instrumental Pop", u"Instrumental Pop",
"Instrumental Rock", u"Instrumental Rock",
"Ethnic", u"Ethnic",
"Gothic", u"Gothic",
"Darkwave", u"Darkwave",
"Techno-Industrial", u"Techno-Industrial",
"Electronic", u"Electronic",
"Pop-Folk", u"Pop-Folk",
"Eurodance", u"Eurodance",
"Dream", u"Dream",
"Southern Rock", u"Southern Rock",
"Comedy", u"Comedy",
"Cult", u"Cult",
"Gangsta Rap", u"Gangsta Rap",
"Top 40", u"Top 40",
"Christian Rap", u"Christian Rap",
"Pop/Funk", u"Pop/Funk",
"Jungle", u"Jungle",
"Native American", u"Native American",
"Cabaret", u"Cabaret",
"New Wave", u"New Wave",
"Psychedelic", u"Psychedelic",
"Rave", u"Rave",
"Showtunes", u"Showtunes",
"Trailer", u"Trailer",
"Lo-Fi", u"Lo-Fi",
"Tribal", u"Tribal",
"Acid Punk", u"Acid Punk",
"Acid Jazz", u"Acid Jazz",
"Polka", u"Polka",
"Retro", u"Retro",
"Musical", u"Musical",
"Rock & Roll", u"Rock & Roll",
"Hard Rock", u"Hard Rock",
"Folk", u"Folk",
"Folk-Rock", u"Folk-Rock",
"National Folk", u"National Folk",
"Swing", u"Swing",
"Fast-Fusion", u"Fast-Fusion",
"Bebop", u"Bebop",
"Latin", u"Latin",
"Revival", u"Revival",
"Celtic", u"Celtic",
"Bluegrass", u"Bluegrass",
"Avantgarde", u"Avantgarde",
"Gothic Rock", u"Gothic Rock",
"Progressive Rock", u"Progressive Rock",
"Psychedelic Rock", u"Psychedelic Rock",
"Symphonic Rock", u"Symphonic Rock",
"Slow Rock", u"Slow Rock",
"Big Band", u"Big Band",
"Chorus", u"Chorus",
"Easy Listening", u"Easy Listening",
"Acoustic", u"Acoustic",
"Humour", u"Humour",
"Speech", u"Speech",
"Chanson", u"Chanson",
"Opera", u"Opera",
"Chamber Music", u"Chamber Music",
"Sonata", u"Sonata",
"Symphony", u"Symphony",
"Booty Bass", u"Booty Bass",
"Primus", u"Primus",
"Porn Groove", u"Porn Groove",
"Satire", u"Satire",
"Slow Jam", u"Slow Jam",
"Club", u"Club",
"Tango", u"Tango",
"Samba", u"Samba",
"Folklore", u"Folklore",
"Ballad", u"Ballad",
"Power Ballad", u"Power Ballad",
"Rhythmic Soul", u"Rhythmic Soul",
"Freestyle", u"Freestyle",
"Duet", u"Duet",
"Punk Rock", u"Punk Rock",
"Drum Solo", u"Drum Solo",
"A Cappella", u"A Cappella",
"Euro-House", u"Euro-House",
"Dance Hall", u"Dance Hall",
"Goa", u"Goa",
"Drum & Bass", u"Drum & Bass",
"Club-House", u"Club-House",
"Hardcore", u"Hardcore",
"Terror", u"Terror",
"Indie", u"Indie",
"BritPop", u"BritPop",
"Afro-Punk", u"Afro-Punk",
"Polsk Punk", u"Polsk Punk",
"Beat", u"Beat",
"Christian Gangsta Rap", u"Christian Gangsta Rap",
"Heavy Metal", u"Heavy Metal",
"Black Metal", u"Black Metal",
"Crossover", u"Crossover",
"Contemporary Christian", u"Contemporary Christian",
"Christian Rock", u"Christian Rock",
"Merengue", u"Merengue",
"Salsa", u"Salsa",
"Thrash Metal", u"Thrash Metal",
"Anime", u"Anime",
"JPop", u"JPop",
"Synthpop", u"Synthpop",
"Abstract", u"Abstract",
"Art Rock", u"Art Rock",
"Baroque", u"Baroque",
"Bhangra", u"Bhangra",
"Big Beat", u"Big Beat",
"Breakbeat", u"Breakbeat",
"Chillout", u"Chillout",
"Downtempo", u"Downtempo",
"Dub", u"Dub",
"EBM", u"EBM",
"Eclectic", u"Eclectic",
"Electro", u"Electro",
"Electroclash", u"Electroclash",
"Emo", u"Emo",
"Experimental", u"Experimental",
"Garage", u"Garage",
"Global", u"Global",
"IDM", u"IDM",
"Illbient", u"Illbient",
"Industro-Goth", u"Industro-Goth",
"Jam Band", u"Jam Band",
"Krautrock", u"Krautrock",
"Leftfield", u"Leftfield",
"Lounge", u"Lounge",
"Math Rock", u"Math Rock",
"New Romantic", u"New Romantic",
"Nu-Breakz", u"Nu-Breakz",
"Post-Punk", u"Post-Punk",
"Post-Rock", u"Post-Rock",
"Psytrance", u"Psytrance",
"Shoegaze", u"Shoegaze",
"Space Rock", u"Space Rock",
"Trop Rock", u"Trop Rock",
"World Music", u"World Music",
"Neoclassical", u"Neoclassical",
"Audiobook", u"Audiobook",
"Audio Theatre", u"Audio Theatre",
"Neue Deutsche Welle", u"Neue Deutsche Welle",
"Podcast", u"Podcast",
"Indie Rock", u"Indie Rock",
"G-Funk", u"G-Funk",
"Dubstep", u"Dubstep",
"Garage Rock", u"Garage Rock",
"Psybient", u"Psybient",
] ]
"""The ID3v1 genre list.""" """The ID3v1 genre list."""
Executable → Regular
+16 -12
View File
@@ -9,7 +9,6 @@
import warnings import warnings
from mutagen._util import DictMixin, loadfile from mutagen._util import DictMixin, loadfile
from mutagen._compat import izip
class FileType(DictMixin): class FileType(DictMixin):
@@ -94,10 +93,10 @@ class FileType(DictMixin):
if self.tags is None: if self.tags is None:
return [] return []
else: else:
return list(self.tags.keys()) return self.tags.keys()
@loadfile(writable=True) @loadfile(writable=True)
def delete(self, filething): def delete(self, filething=None):
"""delete(filething=None) """delete(filething=None)
Remove tags from a file. Remove tags from a file.
@@ -113,14 +112,14 @@ class FileType(DictMixin):
Does nothing if the file has no tags. Does nothing if the file has no tags.
Raises: Raises:
MutagenError: if deleting wasn't possible mutagen.MutagenError: if deleting wasn't possible
""" """
if self.tags is not None: if self.tags is not None:
return self.tags.delete(filething) return self.tags.delete(filething)
@loadfile(writable=True) @loadfile(writable=True)
def save(self, filething, **kwargs): def save(self, filething=None, **kwargs):
"""save(filething=None, **kwargs) """save(filething=None, **kwargs)
Save metadata tags. Save metadata tags.
@@ -150,14 +149,15 @@ class FileType(DictMixin):
"""Adds new tags to the file. """Adds new tags to the file.
Raises: Raises:
MutagenError: if tags already exist or adding is not possible. mutagen.MutagenError:
if tags already exist or adding is not possible.
""" """
raise NotImplementedError raise NotImplementedError
@property @property
def mime(self): def mime(self):
"""A list of mime types (`text`)""" """A list of mime types (:class:`mutagen.text`)"""
mimes = [] mimes = []
for Kind in type(self).__mro__: for Kind in type(self).__mro__:
@@ -171,7 +171,7 @@ class FileType(DictMixin):
"""Returns a score for how likely the file can be parsed by this type. """Returns a score for how likely the file can be parsed by this type.
Args: Args:
filename (path): a file path filename (fspath): a file path
fileobj (fileobj): a file object open in rb mode. Position is fileobj (fileobj): a file object open in rb mode. Position is
undefined undefined
header (bytes): data of undefined length, starts with the start of header (bytes): data of undefined length, starts with the start of
@@ -220,13 +220,13 @@ def File(filething, options=None, easy=False):
filething (filething) filething (filething)
options: Sequence of :class:`FileType` implementations, options: Sequence of :class:`FileType` implementations,
defaults to all included ones. defaults to all included ones.
easy (bool): If the easy wrappers should be returnd if available. easy (bool): If the easy wrappers should be returned if available.
For example :class:`EasyMP3 <mp3.EasyMP3>` instead of For example :class:`EasyMP3 <mp3.EasyMP3>` instead of
:class:`MP3 <mp3.MP3>`. :class:`MP3 <mp3.MP3>`.
Returns: Returns:
FileType: A FileType instance for the detected type or `None` in case FileType: A FileType instance for the detected type or `None` in case
the type couln't be determined. the type couldn't be determined.
Raises: Raises:
MutagenError: in case the detected type fails to load the file. MutagenError: in case the detected type fails to load the file.
@@ -263,12 +263,16 @@ def File(filething, options=None, easy=False):
from mutagen.optimfrog import OptimFROG from mutagen.optimfrog import OptimFROG
from mutagen.aiff import AIFF from mutagen.aiff import AIFF
from mutagen.aac import AAC from mutagen.aac import AAC
from mutagen.ac3 import AC3
from mutagen.smf import SMF from mutagen.smf import SMF
from mutagen.tak import TAK
from mutagen.dsf import DSF from mutagen.dsf import DSF
from mutagen.dsdiff import DSDIFF
from mutagen.wave import WAVE
options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC,
FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack,
Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, AC3,
SMF, DSF] SMF, TAK, DSF, DSDIFF, WAVE]
if not options: if not options:
return None return None
+387
View File
@@ -0,0 +1,387 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014 Evan Purkhiser
# 2014 Ben Ockmore
# 2017 Borewit
# 2019-2020 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""Base classes for various IFF based formats (e.g. AIFF or RIFF)."""
import sys
from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import (
MutagenError,
convert_error,
delete_bytes,
insert_bytes,
loadfile,
reraise,
resize_bytes,
)
class error(MutagenError):
pass
class InvalidChunk(error):
pass
class EmptyChunk(InvalidChunk):
pass
def is_valid_chunk_id(id):
""" is_valid_chunk_id(FOURCC)
Arguments:
id (FOURCC)
Returns:
true if valid; otherwise false
Check if argument id is valid FOURCC type.
"""
assert isinstance(id, str), \
'id is of type %s, must be str: %r' % (type(id), id)
return ((0 < len(id) <= 4) and (min(id) >= ' ') and
(max(id) <= '~'))
# Assert FOURCC formatted valid
def assert_valid_chunk_id(id):
if not is_valid_chunk_id(id):
raise ValueError("IFF chunk ID must be four ASCII characters.")
class IffChunk(object):
"""Generic representation of a single IFF chunk.
IFF chunks always consist of an ID followed by the chunk size. The exact
format varies between different IFF based formats, e.g. AIFF uses
big-endian while RIFF uses little-endian.
"""
# Chunk headers are usually 8 bytes long (4 for ID and 4 for the size)
HEADER_SIZE = 8
@classmethod
def parse_header(cls, header):
"""Read ID and data_size from the given header.
Must be implemented in subclasses."""
raise error("Not implemented")
def write_new_header(self, id_, size):
"""Write the chunk header with id_ and size to the file.
Must be implemented in subclasses. The data must be written
to the current position in self._fileobj."""
raise error("Not implemented")
def write_size(self):
"""Write self.data_size to the file.
Must be implemented in subclasses. The data must be written
to the current position in self._fileobj."""
raise error("Not implemented")
@classmethod
def get_class(cls, id):
"""Returns the class for a new chunk for a given ID.
Can be overridden in subclasses to implement specific chunk types."""
return cls
@classmethod
def parse(cls, fileobj, parent_chunk=None):
header = fileobj.read(cls.HEADER_SIZE)
if len(header) < cls.HEADER_SIZE:
raise EmptyChunk('Header size < %i' % cls.HEADER_SIZE)
id, data_size = cls.parse_header(header)
try:
id = id.decode('ascii').rstrip()
except UnicodeDecodeError as e:
raise InvalidChunk(e)
if not is_valid_chunk_id(id):
raise InvalidChunk('Invalid chunk ID %r' % id)
return cls.get_class(id)(fileobj, id, data_size, parent_chunk)
def __init__(self, fileobj, id, data_size, parent_chunk):
self._fileobj = fileobj
self.id = id
self.data_size = data_size
self.parent_chunk = parent_chunk
self.data_offset = fileobj.tell()
self.offset = self.data_offset - self.HEADER_SIZE
self._calculate_size()
def __repr__(self):
return ("<%s id=%s, offset=%i, size=%i, data_offset=%i, data_size=%i>"
% (type(self).__name__, self.id, self.offset, self.size,
self.data_offset, self.data_size))
def read(self):
"""Read the chunks data"""
self._fileobj.seek(self.data_offset)
return self._fileobj.read(self.data_size)
def write(self, data):
"""Write the chunk data"""
if len(data) > self.data_size:
raise ValueError
self._fileobj.seek(self.data_offset)
self._fileobj.write(data)
# Write the padding bytes
padding = self.padding()
if padding:
self._fileobj.seek(self.data_offset + self.data_size)
self._fileobj.write(b'\x00' * padding)
def delete(self):
"""Removes the chunk from the file"""
delete_bytes(self._fileobj, self.size, self.offset)
if self.parent_chunk is not None:
self.parent_chunk._remove_subchunk(self)
self._fileobj.flush()
def _update_size(self, size_diff, changed_subchunk=None):
"""Update the size of the chunk"""
old_size = self.size
self.data_size += size_diff
self._fileobj.seek(self.offset + 4)
self.write_size()
self._calculate_size()
if self.parent_chunk is not None:
self.parent_chunk._update_size(self.size - old_size, self)
if changed_subchunk:
self._update_sibling_offsets(
changed_subchunk, old_size - self.size)
def _calculate_size(self):
self.size = self.HEADER_SIZE + self.data_size + self.padding()
assert self.size % 2 == 0
def resize(self, new_data_size):
"""Resize the file and update the chunk sizes"""
padding = new_data_size % 2
resize_bytes(self._fileobj, self.data_size + self.padding(),
new_data_size + padding, self.data_offset)
size_diff = new_data_size - self.data_size
self._update_size(size_diff)
self._fileobj.flush()
def padding(self):
"""Returns the number of padding bytes (0 or 1).
IFF chunks are required to be a even number in total length. If
data_size is odd a padding byte will be added at the end.
"""
return self.data_size % 2
class IffContainerChunkMixin():
"""A IFF chunk containing other chunks.
A container chunk can have an additional name as the first 4 bytes of the
chunk data followed by an arbitrary number of subchunks. The root chunk of
the file is always a container chunk (e.g. the AIFF chunk or the FORM chunk
for RIFF) but there can be other types of container chunks (e.g. the LIST
chunks used in RIFF).
"""
def parse_next_subchunk(self):
""""""
raise error("Not implemented")
def init_container(self, name_size=4):
# Lists can store an additional name identifier before the subchunks
self.__name_size = name_size
if self.data_size < name_size:
raise InvalidChunk(
'Container chunk data size < %i' % name_size)
# Read the container name
if name_size > 0:
try:
self.name = self._fileobj.read(name_size).decode('ascii')
except UnicodeDecodeError as e:
raise error(e)
else:
self.name = None
# Load all IFF subchunks
self.__subchunks = []
def subchunks(self):
"""Returns a list of all subchunks.
The list is lazily loaded on first access.
"""
if not self.__subchunks:
next_offset = self.data_offset + self.__name_size
while next_offset < self.offset + self.size:
self._fileobj.seek(next_offset)
try:
chunk = self.parse_next_subchunk()
except EmptyChunk:
break
except InvalidChunk:
break
self.__subchunks.append(chunk)
# Calculate the location of the next chunk
next_offset = chunk.offset + chunk.size
return self.__subchunks
def insert_chunk(self, id_, data=None):
"""Insert a new chunk at the end of the container chunk"""
if not is_valid_chunk_id(id_):
raise KeyError("Invalid IFF key.")
next_offset = self.offset + self.size
size = self.HEADER_SIZE
data_size = 0
if data:
data_size = len(data)
padding = data_size % 2
size += data_size + padding
insert_bytes(self._fileobj, size, next_offset)
self._fileobj.seek(next_offset)
self.write_new_header(id_.ljust(4).encode('ascii'), data_size)
self._fileobj.seek(next_offset)
chunk = self.parse_next_subchunk()
self._update_size(chunk.size)
if data:
chunk.write(data)
self.subchunks().append(chunk)
self._fileobj.flush()
return chunk
def __contains__(self, id_):
"""Check if this chunk contains a specific subchunk."""
assert_valid_chunk_id(id_)
try:
self[id_]
return True
except KeyError:
return False
def __getitem__(self, id_):
"""Get a subchunk by ID."""
assert_valid_chunk_id(id_)
found_chunk = None
for chunk in self.subchunks():
if chunk.id == id_:
found_chunk = chunk
break
else:
raise KeyError("No %r chunk found" % id_)
return found_chunk
def __delitem__(self, id_):
"""Remove a chunk from the IFF file"""
assert_valid_chunk_id(id_)
self[id_].delete()
def _remove_subchunk(self, chunk):
assert chunk in self.__subchunks
self._update_size(-chunk.size, chunk)
self.__subchunks.remove(chunk)
def _update_sibling_offsets(self, changed_subchunk, size_diff):
"""Update the offsets of subchunks after `changed_subchunk`.
"""
index = self.__subchunks.index(changed_subchunk)
sibling_chunks = self.__subchunks[index + 1:len(self.__subchunks)]
for sibling in sibling_chunks:
sibling.offset -= size_diff
sibling.data_offset -= size_diff
class IffFile:
"""Representation of a IFF file"""
def __init__(self, chunk_cls, fileobj):
fileobj.seek(0)
self.root = chunk_cls.parse(fileobj)
def __contains__(self, id_):
"""Check if the IFF file contains a specific chunk"""
return id_ in self.root
def __getitem__(self, id_):
"""Get a chunk from the IFF file"""
return self.root[id_]
def __delitem__(self, id_):
"""Remove a chunk from the IFF file"""
self.delete_chunk(id_)
def delete_chunk(self, id_):
"""Remove a chunk from the IFF file"""
del self.root[id_]
def insert_chunk(self, id_, data=None):
"""Insert a new chunk at the end of the IFF file"""
return self.root.insert_chunk(id_, data)
class IffID3(ID3):
"""A generic IFF file with ID3v2 tags"""
def _load_file(self, fileobj):
raise error("Not implemented")
def _pre_load_header(self, fileobj):
try:
fileobj.seek(self._load_file(fileobj)['ID3'].data_offset)
except (InvalidChunk, KeyError):
raise ID3NoHeaderError("No ID3 chunk")
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething=None, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the IFF file"""
fileobj = filething.fileobj
iff_file = self._load_file(fileobj)
if 'ID3' not in iff_file:
iff_file.insert_chunk('ID3')
chunk = iff_file['ID3']
try:
data = self._prepare_data(
fileobj, chunk.data_offset, chunk.data_size, v2_version,
v23_sep, padding)
except ID3Error as e:
reraise(error, e, sys.exc_info()[2])
chunk.resize(len(data))
chunk.write(data)
@convert_error(IOError, error)
@loadfile(writable=True)
def delete(self, filething=None):
"""Completely removes the ID3 chunk from the IFF file"""
try:
iff_file = self._load_file(filething.fileobj)
del iff_file['ID3']
except KeyError:
pass
self.clear()
+70
View File
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017 Borewit
# Copyright (C) 2019-2020 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""Resource Interchange File Format (RIFF)."""
import struct
from struct import pack
from mutagen._iff import (
IffChunk,
IffContainerChunkMixin,
IffFile,
InvalidChunk,
)
class RiffChunk(IffChunk):
"""Generic RIFF chunk"""
@classmethod
def parse_header(cls, header):
return struct.unpack('<4sI', header)
@classmethod
def get_class(cls, id):
if id in (u'LIST', u'RIFF'):
return RiffListChunk
else:
return cls
def write_new_header(self, id_, size):
self._fileobj.write(pack('<4sI', id_, size))
def write_size(self):
self._fileobj.write(pack('<I', self.data_size))
class RiffListChunk(RiffChunk, IffContainerChunkMixin):
"""A RIFF chunk containing other chunks.
This is either a 'LIST' or 'RIFF'
"""
def parse_next_subchunk(self):
return RiffChunk.parse(self._fileobj, self)
def __init__(self, fileobj, id, data_size, parent_chunk):
if id not in (u'RIFF', u'LIST'):
raise InvalidChunk('Expected RIFF or LIST chunk, got %s' % id)
RiffChunk.__init__(self, fileobj, id, data_size, parent_chunk)
self.init_container()
class RiffFile(IffFile):
"""Representation of a RIFF file"""
def __init__(self, fileobj):
super().__init__(RiffChunk, fileobj)
if self.root.id != u'RIFF':
raise InvalidChunk("Root chunk must be a RIFF chunk, got %s"
% self.root.id)
self.file_type = self.root.name
Executable → Regular
View File
Executable → Regular
+15 -12
View File
@@ -9,18 +9,20 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
import os # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
if os.name != "nt": # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# make imports work # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
_winapi = object() # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from ._fsnative import fsnative, path2fsn, fsn2text, fsn2bytes, \ from ._fsnative import fsnative, path2fsn, fsn2text, fsn2bytes, \
bytes2fsn, uri2fsn, fsn2uri, text2fsn bytes2fsn, uri2fsn, fsn2uri, text2fsn, fsn2norm
from ._print import print_, input_ from ._print import print_, input_, supports_ansi_escape_codes
from ._stdlib import sep, pathsep, curdir, pardir, altsep, extsep, devnull, \ from ._stdlib import sep, pathsep, curdir, pardir, altsep, extsep, devnull, \
defpath, getcwd, expanduser, expandvars defpath, getcwd, expanduser, expandvars
from ._argv import argv from ._argv import argv
@@ -30,10 +32,11 @@ from ._temp import mkstemp, gettempdir, gettempprefix, mkdtemp
fsnative, print_, getcwd, getenv, unsetenv, putenv, environ, expandvars, \ fsnative, print_, getcwd, getenv, unsetenv, putenv, environ, expandvars, \
path2fsn, fsn2text, fsn2bytes, bytes2fsn, uri2fsn, fsn2uri, mkstemp, \ path2fsn, fsn2text, fsn2bytes, bytes2fsn, uri2fsn, fsn2uri, mkstemp, \
gettempdir, gettempprefix, mkdtemp, input_, expanduser, text2fsn gettempdir, gettempprefix, mkdtemp, input_, expanduser, text2fsn, \
supports_ansi_escape_codes, fsn2norm
version = (1, 2, 2) version = (1, 4, 2)
"""Tuple[`int`, `int`, `int`]: The version tuple (major, minor, micro)""" """Tuple[`int`, `int`, `int`]: The version tuple (major, minor, micro)"""
+104
View File
@@ -0,0 +1,104 @@
import sys
import os
from typing import Text, Union, Any, Optional, Tuple, List, Dict
if sys.version_info[0] == 2:
_pathlike = Union[Text, bytes]
else:
_pathlike = Union[Text, bytes, 'os.PathLike[Any]']
_uri = Union[Text, str]
if sys.version_info[0] == 2:
if sys.platform == "win32":
_base = Text
else:
_base = bytes
else:
_base = Text
class fsnative(_base):
def __init__(self, object: Text=u"") -> None:
...
_fsnative = Union[fsnative, _base]
if sys.platform == "win32":
_bytes_default_encoding = str
else:
_bytes_default_encoding = Optional[str]
def path2fsn(path: _pathlike) -> _fsnative:
...
def fsn2text(path: _fsnative, strict: bool=False) -> Text:
...
def text2fsn(text: Text) -> _fsnative:
...
def fsn2bytes(path: _fsnative, encoding: _bytes_default_encoding="utf-8") -> bytes:
...
def bytes2fsn(data: bytes, encoding: _bytes_default_encoding="utf-8") -> _fsnative:
...
def uri2fsn(uri: _uri) -> _fsnative:
...
def fsn2uri(path: _fsnative) -> Text:
...
def fsn2norm(path: _fsnative) -> _fsnative:
...
sep: _fsnative
pathsep: _fsnative
curdir: _fsnative
pardir: _fsnative
altsep: _fsnative
extsep: _fsnative
devnull: _fsnative
defpath: _fsnative
def getcwd() -> _fsnative:
...
def getenv(key: _pathlike, value: Optional[_fsnative]=None) -> Optional[_fsnative]:
...
def putenv(key: _pathlike, value: _pathlike):
...
def unsetenv(key: _pathlike) -> None:
...
def supports_ansi_escape_codes(fd: int) -> bool:
...
def expandvars(path: _pathlike) -> _fsnative:
...
def expanduser(path: _pathlike) -> _fsnative:
...
environ: Dict[_fsnative,_fsnative]
argv: List[_fsnative]
def gettempdir() -> _fsnative:
pass
def mkstemp(suffix: Optional[_pathlike]=None, prefix: Optional[_pathlike]=None, dir: Optional[_pathlike]=None, text: bool=False) -> Tuple[int, _fsnative]:
...
def mkdtemp(suffix: Optional[_pathlike]=None, prefix: Optional[_pathlike]=None, dir: Optional[_pathlike]=None) -> _fsnative:
...
version_string: str
version: Tuple[int, int, int]
print_ = print
def input_(prompt: Any=None) -> _fsnative:
...
Executable → Regular
+15 -4
View File
@@ -9,12 +9,23 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys import sys
import ctypes import ctypes
import collections try:
from collections import abc
except ImportError:
import collections as abc # type: ignore
from functools import total_ordering from functools import total_ordering
from ._compat import PY2, string_types from ._compat import PY2, string_types
@@ -49,7 +60,7 @@ def _get_win_argv():
@total_ordering @total_ordering
class Argv(collections.MutableSequence): class Argv(abc.MutableSequence):
"""List[`fsnative`]: Like `sys.argv` but contains unicode """List[`fsnative`]: Like `sys.argv` but contains unicode
keys and values under Windows + Python 2. keys and values under Windows + Python 2.
Executable → Regular
+18 -13
View File
@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys import sys
@@ -20,26 +28,23 @@ PY3 = not PY2
if PY2: if PY2:
from urllib.parse import urlparse, urlunparse from urlparse import urlparse, urlunparse
urlparse, urlunparse urlparse, urlunparse
from urllib.request import pathname2url, url2pathname from urllib import quote, unquote
from urllib.parse import quote, unquote quote, unquote
pathname2url, url2pathname, quote, unquote
from io import StringIO from StringIO import StringIO
BytesIO = StringIO BytesIO = StringIO
from io import StringIO as TextIO from io import StringIO as TextIO
TextIO TextIO
string_types = (str, str) string_types = (str, unicode)
text_type = str text_type = unicode
iteritems = lambda d: iter(d.items()) iteritems = lambda d: d.iteritems()
elif PY3: elif PY3:
from urllib.parse import urlparse, quote, unquote, urlunparse from urllib.parse import urlparse, quote, unquote, urlunparse
urlparse, quote, unquote, urlunparse urlparse, quote, unquote, urlunparse
from urllib.request import pathname2url, url2pathname
pathname2url, url2pathname
from io import StringIO from io import StringIO
StringIO = StringIO StringIO = StringIO
@@ -50,4 +55,4 @@ elif PY3:
string_types = (str,) string_types = (str,)
text_type = str text_type = str
iteritems = lambda d: iter(list(d.items())) iteritems = lambda d: iter(d.items())
Executable → Regular
+19 -8
View File
@@ -9,12 +9,23 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import os import os
import ctypes import ctypes
import collections try:
from collections import abc
except ImportError:
import collections as abc # type: ignore
from ._compat import text_type, PY2 from ._compat import text_type, PY2
from ._fsnative import path2fsn, is_win, _fsn2legacy, fsnative from ._fsnative import path2fsn, is_win, _fsn2legacy, fsnative
@@ -86,23 +97,23 @@ def read_windows_environ():
res = ctypes.cast(res, ctypes.POINTER(ctypes.c_wchar)) res = ctypes.cast(res, ctypes.POINTER(ctypes.c_wchar))
done = [] done = []
current = "" current = u""
i = 0 i = 0
while 1: while 1:
c = res[i] c = res[i]
i += 1 i += 1
if c == "\x00": if c == u"\x00":
if not current: if not current:
break break
done.append(current) done.append(current)
current = "" current = u""
continue continue
current += c current += c
dict_ = {} dict_ = {}
for entry in done: for entry in done:
try: try:
key, value = entry.split("=", 1) key, value = entry.split(u"=", 1)
except ValueError: except ValueError:
continue continue
key = _norm_key(key) key = _norm_key(key)
@@ -122,7 +133,7 @@ def _norm_key(key):
return key return key
class Environ(collections.MutableMapping): class Environ(abc.MutableMapping):
"""Dict[`fsnative`, `fsnative`]: Like `os.environ` but contains unicode """Dict[`fsnative`, `fsnative`]: Like `os.environ` but contains unicode
keys and values under Windows + Python 2. keys and values under Windows + Python 2.
Executable → Regular
+106 -91
View File
@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import os import os
import sys import sys
@@ -18,8 +26,7 @@ import ctypes
import codecs import codecs
from . import _winapi as winapi from . import _winapi as winapi
from ._compat import text_type, PY3, PY2, url2pathname, urlparse, quote, \ from ._compat import text_type, PY3, PY2, urlparse, quote, unquote, urlunparse
unquote, urlunparse
is_win = os.name == "nt" is_win = os.name == "nt"
@@ -49,45 +56,9 @@ def _swap_bytes(data):
return bytes(data) return bytes(data)
def _codec_fails_on_encode_surrogates(codec, _cache={}): def _decode_surrogatepass(data, codec):
"""Returns if a codec fails correctly when passing in surrogates with
a surrogatepass/surrogateescape error handler. Some codecs were broken
in Python <3.4
"""
try:
return _cache[codec]
except KeyError:
try:
"\uD800\uDC01".encode(codec)
except UnicodeEncodeError:
_cache[codec] = True
else:
_cache[codec] = False
return _cache[codec]
def _codec_can_decode_with_surrogatepass(codec, _cache={}):
"""Returns if a codec supports the surrogatepass error handler when
decoding. Some codecs were broken in Python <3.4
"""
try:
return _cache[codec]
except KeyError:
try:
"\ud83d".encode(
codec, _surrogatepass).decode(codec, _surrogatepass)
except UnicodeDecodeError:
_cache[codec] = False
else:
_cache[codec] = True
return _cache[codec]
def _bytes2winpath(data, codec):
"""Like data.decode(codec, 'surrogatepass') but makes utf-16-le/be work """Like data.decode(codec, 'surrogatepass') but makes utf-16-le/be work
on Python < 3.4 + Windows on Python 2.
https://bugs.python.org/issue27971 https://bugs.python.org/issue27971
@@ -97,7 +68,7 @@ def _bytes2winpath(data, codec):
try: try:
return data.decode(codec, _surrogatepass) return data.decode(codec, _surrogatepass)
except UnicodeDecodeError: except UnicodeDecodeError:
if not _codec_can_decode_with_surrogatepass(codec): if PY2:
if _normalize_codec(codec) == "utf-16-be": if _normalize_codec(codec) == "utf-16-be":
data = _swap_bytes(data) data = _swap_bytes(data)
codec = "utf-16-le" codec = "utf-16-le"
@@ -113,30 +84,45 @@ def _bytes2winpath(data, codec):
raise raise
def _winpath2bytes_py3(text, codec): def _merge_surrogates(text):
"""Fallback implementation for text including surrogates""" """Returns a copy of the text with all surrogate pairs merged"""
# merge surrogate codepoints return _decode_surrogatepass(
if _normalize_codec(codec).startswith("utf-16"):
# fast path, utf-16 merges anyway
return text.encode(codec, _surrogatepass)
return _bytes2winpath(
text.encode("utf-16-le", _surrogatepass), text.encode("utf-16-le", _surrogatepass),
"utf-16-le").encode(codec, _surrogatepass) "utf-16-le")
if PY2: def fsn2norm(path):
def _winpath2bytes(text, codec): """
return text.encode(codec) Args:
path (fsnative): The path to normalize
Returns:
`fsnative`
Normalizes an fsnative path.
The same underlying path can have multiple representations as fsnative
(due to surrogate pairs and variable length encodings). When concatenating
fsnative the result might be different than concatenating the serialized
form and then deserializing it.
This returns the normalized form i.e. the form which os.listdir() would
return. This is useful when you alter fsnative but require that the same
underlying path always maps to the same fsnative value.
All functions like :func:`bytes2fsn`, :func:`fsnative`, :func:`text2fsn`
and :func:`path2fsn` always return a normalized path, independent of their
input.
"""
native = _fsn2native(path)
if is_win:
return _merge_surrogates(native)
elif PY3:
return bytes2fsn(native, None)
else: else:
def _winpath2bytes(text, codec): return path
if _codec_fails_on_encode_surrogates(codec):
try:
return text.encode(codec)
except UnicodeEncodeError:
return _winpath2bytes_py3(text, codec)
else:
return _winpath2bytes_py3(text, codec)
def _fsn2legacy(path): def _fsn2legacy(path):
@@ -173,14 +159,15 @@ def _fsnative(text):
path = text.encode("utf-8", _surrogatepass) path = text.encode("utf-8", _surrogatepass)
if b"\x00" in path: if b"\x00" in path:
path = path.replace(b"\x00", fsn2bytes(_fsnative("\uFFFD"), None)) path = path.replace(b"\x00", fsn2bytes(_fsnative(u"\uFFFD"), None))
if PY3: if PY3:
return path.decode(_encoding, "surrogateescape") return path.decode(_encoding, "surrogateescape")
return path return path
else: else:
if "\x00" in text: if u"\x00" in text:
text = text.replace("\x00", "\uFFFD") text = text.replace(u"\x00", u"\uFFFD")
text = fsn2norm(text)
return text return text
@@ -235,7 +222,7 @@ def _create_fsnative(type_):
the `str` only contains ASCII and no NULL. the `str` only contains ASCII and no NULL.
""" """
def __new__(cls, text=""): def __new__(cls, text=u""):
return _fsnative(text) return _fsnative(text)
new_type = meta("fsnative", (object,), dict(impl.__dict__)) new_type = meta("fsnative", (object,), dict(impl.__dict__))
@@ -259,10 +246,10 @@ def _typecheck_fsnative(path):
return False return False
if PY3 or is_win: if PY3 or is_win:
if "\x00" in path: if u"\x00" in path:
return False return False
if is_unix and not _is_unicode_encoding: if is_unix:
try: try:
path.encode(_encoding, "surrogateescape") path.encode(_encoding, "surrogateescape")
except UnicodeEncodeError: except UnicodeEncodeError:
@@ -297,7 +284,6 @@ def _fsn2native(path):
try: try:
path = path.encode(_encoding, "surrogateescape") path = path.encode(_encoding, "surrogateescape")
except UnicodeEncodeError: except UnicodeEncodeError:
assert not _is_unicode_encoding
# This look more like ValueError, but raising only one error # This look more like ValueError, but raising only one error
# makes things simpler... also one could say str + surrogates # makes things simpler... also one could say str + surrogates
# is its own type # is its own type
@@ -309,7 +295,7 @@ def _fsn2native(path):
if b"\x00" in path: if b"\x00" in path:
raise TypeError("fsnative can't contain nulls") raise TypeError("fsnative can't contain nulls")
else: else:
if "\x00" in path: if u"\x00" in path:
raise TypeError("fsnative can't contain nulls") raise TypeError("fsnative can't contain nulls")
return path return path
@@ -331,7 +317,6 @@ def _get_encoding():
_encoding = _get_encoding() _encoding = _get_encoding()
_is_unicode_encoding = _encoding.startswith("utf")
def path2fsn(path): def path2fsn(path):
@@ -369,9 +354,11 @@ def path2fsn(path):
data = path.encode(_encoding, "surrogateescape") data = path.encode(_encoding, "surrogateescape")
if b"\x00" in data: if b"\x00" in data:
raise ValueError("embedded null") raise ValueError("embedded null")
path = fsn2norm(path)
else: else:
if "\x00" in path: if u"\x00" in path:
raise ValueError("embedded null") raise ValueError("embedded null")
path = fsn2norm(path)
if not isinstance(path, fsnative_type): if not isinstance(path, fsnative_type):
raise TypeError("path needs to be %s", fsnative_type.__name__) raise TypeError("path needs to be %s", fsnative_type.__name__)
@@ -430,22 +417,21 @@ def text2fsn(text):
return fsnative(text) return fsnative(text)
def fsn2bytes(path, encoding): def fsn2bytes(path, encoding="utf-8"):
""" """
Args: Args:
path (fsnative): The path to convert path (fsnative): The path to convert
encoding (`str` or `None`): `None` if you don't care about Windows encoding (`str`): encoding used for Windows
Returns: Returns:
`bytes` `bytes`
Raises: Raises:
TypeError: If no `fsnative` path is passed TypeError: If no `fsnative` path is passed
ValueError: If encoding fails or no encoding is given ValueError: If encoding fails or the encoding is invalid
Converts a `fsnative` path to `bytes`. Converts a `fsnative` path to `bytes`.
The passed *encoding* is only used on platforms where paths are not The passed *encoding* is only used on platforms where paths are not
associated with an encoding (Windows for example). If you don't care about associated with an encoding (Windows for example).
Windows you can pass `None`.
For Windows paths, lone surrogates will be encoded like normal code points For Windows paths, lone surrogates will be encoded like normal code points
and surrogate pairs will be merged before encoding. In case of ``utf-8`` and surrogate pairs will be merged before encoding. In case of ``utf-8``
@@ -459,30 +445,45 @@ def fsn2bytes(path, encoding):
if encoding is None: if encoding is None:
raise ValueError("invalid encoding %r" % encoding) raise ValueError("invalid encoding %r" % encoding)
if PY2:
try: try:
return _winpath2bytes(path, encoding) return path.encode(encoding)
except LookupError: except LookupError:
raise ValueError("invalid encoding %r" % encoding) raise ValueError("invalid encoding %r" % encoding)
else:
try:
return path.encode(encoding)
except LookupError:
raise ValueError("invalid encoding %r" % encoding)
except UnicodeEncodeError:
# Fallback implementation for text including surrogates
# merge surrogate codepoints
if _normalize_codec(encoding).startswith("utf-16"):
# fast path, utf-16 merges anyway
return path.encode(encoding, _surrogatepass)
return _merge_surrogates(path).encode(encoding, _surrogatepass)
else: else:
return path return path
def bytes2fsn(data, encoding): def bytes2fsn(data, encoding="utf-8"):
""" """
Args: Args:
data (bytes): The data to convert data (bytes): The data to convert
encoding (`str` or `None`): `None` if you don't care about Windows encoding (`str`): encoding used for Windows
Returns: Returns:
`fsnative` `fsnative`
Raises: Raises:
TypeError: If no `bytes` path is passed TypeError: If no `bytes` path is passed
ValueError: If decoding fails or no encoding is given ValueError: If decoding fails or the encoding is invalid
Turns `bytes` to a `fsnative` path. Turns `bytes` to a `fsnative` path.
The passed *encoding* is only used on platforms where paths are not The passed *encoding* is only used on platforms where paths are not
associated with an encoding (Windows for example). If you don't care about associated with an encoding (Windows for example).
Windows you can pass `None`.
For Windows paths ``WTF-8`` is accepted if ``utf-8`` is used and
``WTF-16`` accepted if ``utf-16-le`` is used.
""" """
if not isinstance(data, bytes): if not isinstance(data, bytes):
@@ -492,10 +493,10 @@ def bytes2fsn(data, encoding):
if encoding is None: if encoding is None:
raise ValueError("invalid encoding %r" % encoding) raise ValueError("invalid encoding %r" % encoding)
try: try:
path = _bytes2winpath(data, encoding) path = _decode_surrogatepass(data, encoding)
except LookupError: except LookupError:
raise ValueError("invalid encoding %r" % encoding) raise ValueError("invalid encoding %r" % encoding)
if "\x00" in path: if u"\x00" in path:
raise ValueError("contains nulls") raise ValueError("contains nulls")
return path return path
else: else:
@@ -543,20 +544,32 @@ def uri2fsn(uri):
uri = urlunparse(parsed)[7:] uri = urlunparse(parsed)[7:]
if is_win: if is_win:
path = url2pathname(uri) try:
drive, rest = uri.split(":", 1)
except ValueError:
path = ""
rest = uri.replace("/", "\\")
else:
path = drive[-1] + ":"
rest = rest.replace("/", "\\")
if PY2:
path += unquote(rest)
else:
path += unquote(rest, encoding="utf-8", errors="surrogatepass")
if netloc: if netloc:
path = "\\\\" + path path = "\\\\" + path
if PY2: if PY2:
path = path.decode("utf-8") path = path.decode("utf-8")
if "\x00" in path: if u"\x00" in path:
raise ValueError("embedded null") raise ValueError("embedded null")
return path return path
else: else:
path = url2pathname(uri) if PY2:
path = unquote(uri)
else:
path = unquote(uri, encoding=_encoding, errors="surrogateescape")
if "\x00" in path: if "\x00" in path:
raise ValueError("embedded null") raise ValueError("embedded null")
if PY3:
path = fsnative(path)
return path return path
@@ -594,6 +607,8 @@ def fsn2uri(path):
except WindowsError as e: except WindowsError as e:
raise ValueError(e) raise ValueError(e)
uri = buf[:length.value] uri = buf[:length.value]
# https://bitbucket.org/pypy/pypy/issues/3133
uri = _merge_surrogates(uri)
# For some reason UrlCreateFromPathW escapes some chars outside of # For some reason UrlCreateFromPathW escapes some chars outside of
# ASCII and some not. Unquote and re-quote with utf-8. # ASCII and some not. Unquote and re-quote with utf-8.
@@ -607,4 +622,4 @@ def fsn2uri(path):
return _quote_path(uri.encode("utf-8", _surrogatepass)) return _quote_path(uri.encode("utf-8", _surrogatepass))
else: else:
return "file://" + _quote_path(path) return u"file://" + _quote_path(path)
Executable → Regular
+78 -7
View File
@@ -9,14 +9,23 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys import sys
import os import os
import ctypes import ctypes
import re
from ._fsnative import _encoding, is_win, is_unix, _surrogatepass from ._fsnative import _encoding, is_win, is_unix, _surrogatepass, bytes2fsn
from ._compat import text_type, PY2, PY3 from ._compat import text_type, PY2, PY3
from ._winansi import AnsiState, ansi_split from ._winansi import AnsiState, ansi_split
from . import _winapi as winapi from . import _winapi as winapi
@@ -155,7 +164,7 @@ def _print_windows(objects, sep, end, file, flush):
if not isinstance(end, text_type): if not isinstance(end, text_type):
raise TypeError raise TypeError
if end == "\n": if end == u"\n":
end = os.linesep end = os.linesep
text = sep.join(parts) + end text = sep.join(parts) + end
@@ -225,7 +234,7 @@ def _readline_windows():
buf = ctypes.create_string_buffer(buf_size * ctypes.sizeof(winapi.WCHAR)) buf = ctypes.create_string_buffer(buf_size * ctypes.sizeof(winapi.WCHAR))
read = winapi.DWORD() read = winapi.DWORD()
text = "" text = u""
while True: while True:
if winapi.ReadConsoleW( if winapi.ReadConsoleW(
h, buf, buf_size, ctypes.byref(read), None) == 0: h, buf, buf_size, ctypes.byref(read), None) == 0:
@@ -234,7 +243,7 @@ def _readline_windows():
raise ctypes.WinError() raise ctypes.WinError()
data = buf[:read.value * ctypes.sizeof(winapi.WCHAR)] data = buf[:read.value * ctypes.sizeof(winapi.WCHAR)]
text += data.decode("utf-16-le", _surrogatepass) text += data.decode("utf-16-le", _surrogatepass)
if text.endswith("\r\n"): if text.endswith(u"\r\n"):
return text[:-2] return text[:-2]
@@ -253,7 +262,7 @@ def _decode_codepage(codepage, data):
assert isinstance(data, bytes) assert isinstance(data, bytes)
if not data: if not data:
return "" return u""
# get the required buffer length first # get the required buffer length first
length = winapi.MultiByteToWideChar(codepage, 0, data, len(data), None, 0) length = winapi.MultiByteToWideChar(codepage, 0, data, len(data), None, 0)
@@ -351,3 +360,65 @@ def input_(prompt=None):
print_(prompt, end="") print_(prompt, end="")
return _readline() return _readline()
def _get_file_name_for_handle(handle):
"""(Windows only) Returns a file name for a file handle.
Args:
handle (winapi.HANDLE)
Returns:
`text` or `None` if no file name could be retrieved.
"""
assert is_win
assert handle != winapi.INVALID_HANDLE_VALUE
size = winapi.FILE_NAME_INFO.FileName.offset + \
winapi.MAX_PATH * ctypes.sizeof(winapi.WCHAR)
buf = ctypes.create_string_buffer(size)
if winapi.GetFileInformationByHandleEx is None:
# Windows XP
return None
status = winapi.GetFileInformationByHandleEx(
handle, winapi.FileNameInfo, buf, size)
if status == 0:
return None
name_info = ctypes.cast(
buf, ctypes.POINTER(winapi.FILE_NAME_INFO)).contents
offset = winapi.FILE_NAME_INFO.FileName.offset
data = buf[offset:offset + name_info.FileNameLength]
return bytes2fsn(data, "utf-16-le")
def supports_ansi_escape_codes(fd):
"""Returns whether the output device is capable of interpreting ANSI escape
codes when :func:`print_` is used.
Args:
fd (int): file descriptor (e.g. ``sys.stdout.fileno()``)
Returns:
`bool`
"""
if os.isatty(fd):
return True
if not is_win:
return False
# Check for cygwin/msys terminal
handle = winapi._get_osfhandle(fd)
if handle == winapi.INVALID_HANDLE_VALUE:
return False
if winapi.GetFileType(handle) != winapi.FILE_TYPE_PIPE:
return False
file_name = _get_file_name_for_handle(handle)
match = re.match(
"^\\\\(cygwin|msys)-[a-z0-9]+-pty[0-9]+-(from|to)-master$", file_name)
return match is not None
Executable → Regular
+11 -3
View File
@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import re import re
import os import os
@@ -38,7 +46,7 @@ def getcwd():
""" """
if is_win and PY2: if is_win and PY2:
return os.getcwd() return os.getcwdu()
return os.getcwd() return os.getcwd()
Executable → Regular
+10 -2
View File
@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import tempfile import tempfile
Executable → Regular
+11 -3
View File
@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import ctypes import ctypes
import re import re
@@ -25,7 +33,7 @@ def ansi_parse(code):
return code[-1:], tuple([int(v or "0") for v in code[2:-1].split(";")]) return code[-1:], tuple([int(v or "0") for v in code[2:-1].split(";")])
def ansi_split(text, _re=re.compile("(\x1b\[(\d*;?)*\S)")): def ansi_split(text, _re=re.compile(u"(\x1b\\[(\\d*;?)*\\S)")):
"""Yields (is_ansi, text)""" """Yields (is_ansi, text)"""
for part in _re.split(text): for part in _re.split(text):
Executable → Regular
+48 -10
View File
@@ -9,16 +9,27 @@
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included
# included in all copies or substantial portions of the Software. # in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
import ctypes import ctypes
from ctypes import WinDLL, wintypes
if sys.platform == 'win32':
from ctypes import WinDLL, CDLL, wintypes
shell32 = WinDLL("shell32") shell32 = WinDLL("shell32")
kernel32 = WinDLL("kernel32") kernel32 = WinDLL("kernel32")
shlwapi = WinDLL("shlwapi") shlwapi = WinDLL("shlwapi")
msvcrt = CDLL("msvcrt")
GetCommandLineW = kernel32.GetCommandLineW GetCommandLineW = kernel32.GetCommandLineW
GetCommandLineW.argtypes = [] GetCommandLineW.argtypes = []
@@ -105,7 +116,6 @@ GetStdHandle = kernel32.GetStdHandle
GetStdHandle.argtypes = [DWORD] GetStdHandle.argtypes = [DWORD]
GetStdHandle.restype = HANDLE GetStdHandle.restype = HANDLE
class COORD(ctypes.Structure): class COORD(ctypes.Structure):
_fields_ = [ _fields_ = [
@@ -113,7 +123,6 @@ class COORD(ctypes.Structure):
("Y", SHORT), ("Y", SHORT),
] ]
class SMALL_RECT(ctypes.Structure): class SMALL_RECT(ctypes.Structure):
_fields_ = [ _fields_ = [
@@ -123,7 +132,6 @@ class SMALL_RECT(ctypes.Structure):
("Bottom", SHORT), ("Bottom", SHORT),
] ]
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
_fields_ = [ _fields_ = [
@@ -134,7 +142,6 @@ class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
("dwMaximumWindowSize", COORD), ("dwMaximumWindowSize", COORD),
] ]
GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo
GetConsoleScreenBufferInfo.argtypes = [ GetConsoleScreenBufferInfo.argtypes = [
HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)]
@@ -165,7 +172,8 @@ SetConsoleCursorPosition.argtypes = [HANDLE, COORD]
SetConsoleCursorPosition.restype = BOOL SetConsoleCursorPosition.restype = BOOL
ReadConsoleW = kernel32.ReadConsoleW ReadConsoleW = kernel32.ReadConsoleW
ReadConsoleW.argtypes = [HANDLE, LPVOID, DWORD, ctypes.POINTER(DWORD), LPVOID] ReadConsoleW.argtypes = [
HANDLE, LPVOID, DWORD, ctypes.POINTER(DWORD), LPVOID]
ReadConsoleW.restype = BOOL ReadConsoleW.restype = BOOL
MultiByteToWideChar = kernel32.MultiByteToWideChar MultiByteToWideChar = kernel32.MultiByteToWideChar
@@ -175,9 +183,39 @@ MultiByteToWideChar.restype = ctypes.c_int
WideCharToMultiByte = kernel32.WideCharToMultiByte WideCharToMultiByte = kernel32.WideCharToMultiByte
WideCharToMultiByte.argtypes = [ WideCharToMultiByte.argtypes = [
UINT, DWORD, LPCWSTR, ctypes.c_int, LPSTR, ctypes.c_int, LPCSTR, LPBOOL] UINT, DWORD, LPCWSTR, ctypes.c_int, LPSTR, ctypes.c_int,
WideCharToMultiByte.restpye = ctypes.c_int LPCSTR, LPBOOL]
WideCharToMultiByte.restype = ctypes.c_int
MoveFileW = kernel32.MoveFileW MoveFileW = kernel32.MoveFileW
MoveFileW.argtypes = [LPCTSTR, LPCTSTR] MoveFileW.argtypes = [LPCTSTR, LPCTSTR]
MoveFileW.restype = BOOL MoveFileW.restype = BOOL
GetFileInformationByHandleEx = None
if hasattr(kernel32, "GetFileInformationByHandleEx"):
GetFileInformationByHandleEx = kernel32.GetFileInformationByHandleEx
GetFileInformationByHandleEx.argtypes = [
HANDLE, ctypes.c_int, ctypes.c_void_p, DWORD]
GetFileInformationByHandleEx.restype = BOOL
else:
# Windows XP
pass
MAX_PATH = 260
FileNameInfo = 2
class FILE_NAME_INFO(ctypes.Structure):
_fields_ = [
("FileNameLength", DWORD),
("FileName", WCHAR),
]
_get_osfhandle = msvcrt._get_osfhandle
_get_osfhandle.argtypes = [ctypes.c_int]
_get_osfhandle.restype = HANDLE
GetFileType = kernel32.GetFileType
GetFileType.argtypes = [HANDLE]
GetFileType.restype = DWORD
FILE_TYPE_PIPE = 0x0003
View File
Executable → Regular
+2 -2
View File
@@ -115,7 +115,7 @@ class Metadata(Tags):
raise NotImplementedError raise NotImplementedError
@loadfile(writable=False) @loadfile(writable=False)
def save(self, filething, **kwargs): def save(self, filething=None, **kwargs):
"""save(filething=None, **kwargs) """save(filething=None, **kwargs)
Save changes to a file. Save changes to a file.
@@ -129,7 +129,7 @@ class Metadata(Tags):
raise NotImplementedError raise NotImplementedError
@loadfile(writable=False) @loadfile(writable=False)
def delete(self, filething): def delete(self, filething=None):
"""delete(filething=None) """delete(filething=None)
Remove tags from a file. Remove tags from a file.
Executable → Regular
View File
Executable → Regular
+2 -2
View File
@@ -12,7 +12,7 @@ import contextlib
import optparse import optparse
from mutagen._senf import print_ from mutagen._senf import print_
from mutagen._compat import text_type, iterbytes from mutagen._util import iterbytes
def split_escape(string, sep, maxsplit=None, escape_char="\\"): def split_escape(string, sep, maxsplit=None, escape_char="\\"):
@@ -25,7 +25,7 @@ def split_escape(string, sep, maxsplit=None, escape_char="\\"):
assert len(escape_char) == 1 assert len(escape_char) == 1
if isinstance(string, bytes): if isinstance(string, bytes):
if isinstance(escape_char, text_type): if isinstance(escape_char, str):
escape_char = escape_char.encode("ascii") escape_char = escape_char.encode("ascii")
iter_ = iterbytes iter_ = iterbytes
else: else:
Executable → Regular
+7 -8
View File
@@ -16,7 +16,6 @@ import os.path
import mutagen import mutagen
import mutagen.id3 import mutagen.id3
from mutagen._senf import print_, argv from mutagen._senf import print_, argv
from mutagen._compat import text_type
from ._util import SignalHandler, OptionParser from ._util import SignalHandler, OptionParser
@@ -52,14 +51,14 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False):
try: try:
id3 = mutagen.id3.ID3(src, translate=False) id3 = mutagen.id3.ID3(src, translate=False)
except mutagen.id3.ID3NoHeaderError: except mutagen.id3.ID3NoHeaderError:
print_("No ID3 header found in ", src, file=sys.stderr) print_(u"No ID3 header found in ", src, file=sys.stderr)
return 1 return 1
except Exception as err: except Exception as err:
print_(str(err), file=sys.stderr) print_(str(err), file=sys.stderr)
return 1 return 1
if verbose: if verbose:
print_("File", src, "contains:", file=sys.stderr) print_(u"File", src, u"contains:", file=sys.stderr)
print_(id3.pprint(), file=sys.stderr) print_(id3.pprint(), file=sys.stderr)
for tag in excluded_tags: for tag in excluded_tags:
@@ -75,7 +74,7 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False):
print_(str(err), file=sys.stderr) print_(str(err), file=sys.stderr)
return 1 return 1
else: else:
for frame in list(id3.values()): for frame in id3.values():
target.add(frame) target.add(frame)
id3 = target id3 = target
@@ -91,12 +90,12 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False):
try: try:
id3.save(dst, v1=(2 if write_v1 else 0), v2_version=v2_version) id3.save(dst, v1=(2 if write_v1 else 0), v2_version=v2_version)
except Exception as err: except Exception as err:
print_("Error saving", dst, ":\n%s" % text_type(err), print_(u"Error saving", dst, u":\n%s" % str(err),
file=sys.stderr) file=sys.stderr)
return 1 return 1
else: else:
if verbose: if verbose:
print_("Successfully saved", dst, file=sys.stderr) print_(u"Successfully saved", dst, file=sys.stderr)
return 0 return 0
@@ -120,12 +119,12 @@ def main(argv):
(src, dst) = args (src, dst) = args
if not os.path.isfile(src): if not os.path.isfile(src):
print_("File not found:", src, file=sys.stderr) print_(u"File not found:", src, file=sys.stderr)
parser.print_help(file=sys.stderr) parser.print_help(file=sys.stderr)
return 1 return 1
if not os.path.isfile(dst): if not os.path.isfile(dst):
printerr("File not found:", dst, file=sys.stderr) printerr(u"File not found:", dst, file=sys.stderr)
parser.print_help(file=sys.stderr) parser.print_help(file=sys.stderr)
return 1 return 1
Executable → Regular
+8 -9
View File
@@ -16,7 +16,6 @@ import locale
import mutagen import mutagen
import mutagen.id3 import mutagen.id3
from mutagen._senf import argv, print_, fsnative from mutagen._senf import argv, print_, fsnative
from mutagen._compat import text_type
from ._util import SignalHandler, OptionParser from ._util import SignalHandler, OptionParser
@@ -75,7 +74,7 @@ def update(options, filenames):
for filename in filenames: for filename in filenames:
with _sig.block(): with _sig.block():
if verbose != "quiet": if verbose != "quiet":
print_("Updating", filename) print_(u"Updating", filename)
if has_id3v1(filename) and not noupdate and force_v1: if has_id3v1(filename) and not noupdate and force_v1:
mutagen.id3.delete(filename, False, True) mutagen.id3.delete(filename, False, True)
@@ -84,13 +83,13 @@ def update(options, filenames):
id3 = mutagen.id3.ID3(filename) id3 = mutagen.id3.ID3(filename)
except mutagen.id3.ID3NoHeaderError: except mutagen.id3.ID3NoHeaderError:
if verbose != "quiet": if verbose != "quiet":
print_("No ID3 header found; skipping...") print_(u"No ID3 header found; skipping...")
continue continue
except Exception as err: except Exception as err:
print_(text_type(err), file=sys.stderr) print_(str(err), file=sys.stderr)
continue continue
for tag in [t for t in id3 if t.startswith(("T", "COMM"))]: for tag in filter(lambda t: t.startswith(("T", "COMM")), id3):
frame = id3[tag] frame = id3[tag]
if isinstance(frame, mutagen.id3.TimeStampTextFrame): if isinstance(frame, mutagen.id3.TimeStampTextFrame):
# non-unicode fields # non-unicode fields
@@ -105,7 +104,7 @@ def update(options, filenames):
continue continue
else: else:
frame.text = text frame.text = text
if not text or min(list(map(isascii, text))): if not text or min(map(isascii, text)):
frame.encoding = 3 frame.encoding = 3
else: else:
frame.encoding = 1 frame.encoding = 1
@@ -122,7 +121,7 @@ def update(options, filenames):
def has_id3v1(filename): def has_id3v1(filename):
try: try:
with open(filename, 'rb+') as f: with open(filename, 'rb') as f:
f.seek(-128, 2) f.seek(-128, 2)
return f.read(3) == b"TAG" return f.read(3) == b"TAG"
except IOError: except IOError:
@@ -154,9 +153,9 @@ def main(argv):
for i, arg in enumerate(argv): for i, arg in enumerate(argv):
if arg == "-v1": if arg == "-v1":
argv[i] = fsnative("--force-v1") argv[i] = fsnative(u"--force-v1")
elif arg == "-removev1": elif arg == "-removev1":
argv[i] = fsnative("--remove-v1") argv[i] = fsnative(u"--remove-v1")
(options, args) = parser.parse_args(argv[1:]) (options, args) = parser.parse_args(argv[1:])
Executable → Regular
+70 -47
View File
@@ -11,6 +11,7 @@
import sys import sys
import codecs import codecs
import mimetypes import mimetypes
import warnings
from optparse import SUPPRESS_HELP from optparse import SUPPRESS_HELP
@@ -19,7 +20,6 @@ import mutagen.id3
from mutagen.id3 import Encoding, PictureType from mutagen.id3 import Encoding, PictureType
from mutagen._senf import fsnative, print_, argv, fsn2text, fsn2bytes, \ from mutagen._senf import fsnative, print_, argv, fsn2text, fsn2bytes, \
bytes2fsn bytes2fsn
from mutagen._compat import PY2, text_type
from ._util import split_escape, SignalHandler, OptionParser from ._util import split_escape, SignalHandler, OptionParser
@@ -55,23 +55,23 @@ Any editing operation will cause the ID3 tag to be upgraded to ID3v2.4.
def list_frames(option, opt, value, parser): def list_frames(option, opt, value, parser):
items = list(mutagen.id3.Frames.items()) items = mutagen.id3.Frames.items()
for name, frame in sorted(items): for name, frame in sorted(items):
print_(" --%s %s" % (name, frame.__doc__.split("\n")[0])) print_(u" --%s %s" % (name, frame.__doc__.split("\n")[0]))
raise SystemExit raise SystemExit
def list_frames_2_2(option, opt, value, parser): def list_frames_2_2(option, opt, value, parser):
items = list(mutagen.id3.Frames_2_2.items()) items = mutagen.id3.Frames_2_2.items()
items.sort() items.sort()
for name, frame in items: for name, frame in items:
print_(" --%s %s" % (name, frame.__doc__.split("\n")[0])) print_(u" --%s %s" % (name, frame.__doc__.split("\n")[0]))
raise SystemExit raise SystemExit
def list_genres(option, opt, value, parser): def list_genres(option, opt, value, parser):
for i, genre in enumerate(mutagen.id3.TCON.GENRES): for i, genre in enumerate(mutagen.id3.TCON.GENRES):
print_("%3d: %s" % (i, genre)) print_(u"%3d: %s" % (i, genre))
raise SystemExit raise SystemExit
@@ -79,7 +79,7 @@ def delete_tags(filenames, v1, v2):
for filename in filenames: for filename in filenames:
with _sig.block(): with _sig.block():
if verbose: if verbose:
print_("deleting ID3 tag info in", filename, file=sys.stderr) print_(u"deleting ID3 tag info in", filename, file=sys.stderr)
mutagen.id3.delete(filename, v1, v2) mutagen.id3.delete(filename, v1, v2)
@@ -88,22 +88,22 @@ def delete_frames(deletes, filenames):
try: try:
deletes = frame_from_fsnative(deletes) deletes = frame_from_fsnative(deletes)
except ValueError as err: except ValueError as err:
print_(text_type(err), file=sys.stderr) print_(str(err), file=sys.stderr)
frames = deletes.split(",") frames = deletes.split(",")
for filename in filenames: for filename in filenames:
with _sig.block(): with _sig.block():
if verbose: if verbose:
print_("deleting %s from" % deletes, filename, print_(u"deleting %s from" % deletes, filename,
file=sys.stderr) file=sys.stderr)
try: try:
id3 = mutagen.id3.ID3(filename) id3 = mutagen.id3.ID3(filename)
except mutagen.id3.ID3NoHeaderError: except mutagen.id3.ID3NoHeaderError:
if verbose: if verbose:
print_("No ID3 header found; skipping.", file=sys.stderr) print_(u"No ID3 header found; skipping.", file=sys.stderr)
except Exception as err: except Exception as err:
print_(text_type(err), file=sys.stderr) print_(str(err), file=sys.stderr)
raise SystemExit(1) raise SystemExit(1)
else: else:
for frame in frames: for frame in frames:
@@ -119,26 +119,24 @@ def frame_from_fsnative(arg):
assert isinstance(arg, fsnative) assert isinstance(arg, fsnative)
text = fsn2text(arg, strict=True) text = fsn2text(arg, strict=True)
if PY2:
return text.encode("ascii")
else:
return text.encode("ascii").decode("ascii") return text.encode("ascii").decode("ascii")
def value_from_fsnative(arg, escape): def value_from_fsnative(arg, escape):
"""Takes an item from argv and returns a text_type value without """Takes an item from argv and returns a str value without
surrogate escapes or raises ValueError. surrogate escapes or raises ValueError.
""" """
assert isinstance(arg, fsnative) assert isinstance(arg, fsnative)
if escape: if escape:
bytes_ = fsn2bytes(arg, "utf-8") bytes_ = fsn2bytes(arg)
if PY2: # With py3.7 this has started to warn for invalid escapes, but we
bytes_ = bytes_.decode("string_escape") # don't control the input so ignore it.
else: with warnings.catch_warnings():
warnings.simplefilter("ignore")
bytes_ = codecs.escape_decode(bytes_)[0] bytes_ = codecs.escape_decode(bytes_)[0]
arg = bytes2fsn(bytes_, "utf-8") arg = bytes2fsn(bytes_)
text = fsn2text(arg, strict=True) text = fsn2text(arg, strict=True)
return text return text
@@ -167,7 +165,7 @@ def write_files(edits, filenames, escape):
try: try:
frame = frame_from_fsnative(frame) frame = frame_from_fsnative(frame)
except ValueError as err: except ValueError as err:
print_(text_type(err), file=sys.stderr) print_(str(err), file=sys.stderr)
assert isinstance(frame, str) assert isinstance(frame, str)
@@ -177,9 +175,9 @@ def write_files(edits, filenames, escape):
try: try:
value = value_from_fsnative(value, escape) value = value_from_fsnative(value, escape)
except ValueError as err: except ValueError as err:
error("%s: %s" % (frame, text_type(err))) error(u"%s: %s" % (frame, str(err)))
assert isinstance(value, text_type) assert isinstance(value, str)
encoded_edits.append((frame, value)) encoded_edits.append((frame, value))
edits = encoded_edits edits = encoded_edits
@@ -205,18 +203,18 @@ def write_files(edits, filenames, escape):
for filename in filenames: for filename in filenames:
with _sig.block(): with _sig.block():
if verbose: if verbose:
print_("Writing", filename, file=sys.stderr) print_(u"Writing", filename, file=sys.stderr)
try: try:
id3 = mutagen.id3.ID3(filename) id3 = mutagen.id3.ID3(filename)
except mutagen.id3.ID3NoHeaderError: except mutagen.id3.ID3NoHeaderError:
if verbose: if verbose:
print_("No ID3 header found; creating a new tag", print_(u"No ID3 header found; creating a new tag",
file=sys.stderr) file=sys.stderr)
id3 = mutagen.id3.ID3() id3 = mutagen.id3.ID3()
except Exception as err: except Exception as err:
print_(str(err), file=sys.stderr) print_(str(err), file=sys.stderr)
continue continue
for (frame, vlist) in list(edits.items()): for (frame, vlist) in edits.items():
if frame == "POPM": if frame == "POPM":
for value in vlist: for value in vlist:
values = string_split(value, ":") values = string_split(value, ":")
@@ -240,13 +238,13 @@ def write_files(edits, filenames, escape):
if len(values) >= 2: if len(values) >= 2:
desc = values[1] desc = values[1]
else: else:
desc = "cover" desc = u"cover"
if len(values) >= 3: if len(values) >= 3:
try: try:
picture_type = int(values[2]) picture_type = int(values[2])
except ValueError: except ValueError:
error("Invalid picture type: %r" % values[1]) error(u"Invalid picture type: %r" % values[1])
else: else:
picture_type = PictureType.COVER_FRONT picture_type = PictureType.COVER_FRONT
@@ -264,7 +262,7 @@ def write_files(edits, filenames, escape):
with open(fn, "rb") as h: with open(fn, "rb") as h:
data = h.read() data = h.read()
except IOError as e: except IOError as e:
error(text_type(e)) error(str(e))
frame = mutagen.id3.APIC(encoding=encoding, mime=mime, frame = mutagen.id3.APIC(encoding=encoding, mime=mime,
desc=desc, type=picture_type, data=data) desc=desc, type=picture_type, data=data)
@@ -283,11 +281,24 @@ def write_files(edits, filenames, escape):
frame = mutagen.id3.COMM( frame = mutagen.id3.COMM(
encoding=3, text=value, lang=lang, desc=desc) encoding=3, text=value, lang=lang, desc=desc)
id3.add(frame) id3.add(frame)
elif frame == "USLT":
for value in vlist:
values = string_split(value, ":")
if len(values) == 1:
value, desc, lang = values[0], "", "eng"
elif len(values) == 2:
desc, value, lang = values[0], values[1], "eng"
else:
value = ":".join(values[1:-1])
desc, lang = values[0], values[-1]
frame = mutagen.id3.USLT(
encoding=3, text=value, lang=lang, desc=desc)
id3.add(frame)
elif frame == "UFID": elif frame == "UFID":
for value in vlist: for value in vlist:
values = string_split(value, ":") values = string_split(value, ":")
if len(values) != 2: if len(values) != 2:
error("Invalid value: %r" % values) error(u"Invalid value: %r" % values)
owner = values[0] owner = values[0]
data = values[1].encode("utf-8") data = values[1].encode("utf-8")
frame = mutagen.id3.UFID(owner=owner, data=data) frame = mutagen.id3.UFID(owner=owner, data=data)
@@ -302,9 +313,20 @@ def write_files(edits, filenames, escape):
frame = mutagen.id3.TXXX( frame = mutagen.id3.TXXX(
encoding=3, text=value, desc=desc) encoding=3, text=value, desc=desc)
id3.add(frame) id3.add(frame)
elif frame == "WXXX":
for value in vlist:
values = string_split(value, ":", 1)
if len(values) == 1:
desc, value = "", values[0]
else:
desc, value = values[0], values[1]
frame = mutagen.id3.WXXX(
encoding=3, url=value, desc=desc)
id3.add(frame)
elif issubclass(mutagen.id3.Frames[frame], elif issubclass(mutagen.id3.Frames[frame],
mutagen.id3.UrlFrame): mutagen.id3.UrlFrame):
frame = mutagen.id3.Frames[frame](encoding=3, url=vlist) frame = mutagen.id3.Frames[frame](
encoding=3, url=vlist[-1])
id3.add(frame) id3.add(frame)
else: else:
frame = mutagen.id3.Frames[frame](encoding=3, text=vlist) frame = mutagen.id3.Frames[frame](encoding=3, text=vlist)
@@ -318,9 +340,9 @@ def list_tags(filenames):
try: try:
id3 = mutagen.id3.ID3(filename, translate=False) id3 = mutagen.id3.ID3(filename, translate=False)
except mutagen.id3.ID3NoHeaderError: except mutagen.id3.ID3NoHeaderError:
print_("No ID3 header found; skipping.") print_(u"No ID3 header found; skipping.")
except Exception as err: except Exception as err:
print_(text_type(err), file=sys.stderr) print_(str(err), file=sys.stderr)
raise SystemExit(1) raise SystemExit(1)
else: else:
print_(id3.pprint()) print_(id3.pprint())
@@ -332,13 +354,13 @@ def list_tags_raw(filenames):
try: try:
id3 = mutagen.id3.ID3(filename, translate=False) id3 = mutagen.id3.ID3(filename, translate=False)
except mutagen.id3.ID3NoHeaderError: except mutagen.id3.ID3NoHeaderError:
print_("No ID3 header found; skipping.") print_(u"No ID3 header found; skipping.")
except Exception as err: except Exception as err:
print_(text_type(err), file=sys.stderr) print_(str(err), file=sys.stderr)
raise SystemExit(1) raise SystemExit(1)
else: else:
for frame in list(id3.values()): for frame in id3.values():
print_(text_type(repr(frame))) print_(str(repr(frame)))
def main(argv): def main(argv):
@@ -387,50 +409,51 @@ def main(argv):
parser.add_option( parser.add_option(
"-a", "--artist", metavar='"ARTIST"', action="callback", "-a", "--artist", metavar='"ARTIST"', action="callback",
help="Set the artist information", type="string", help="Set the artist information", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TPE1"), callback=lambda *args: args[3].edits.append((fsnative(u"--TPE1"),
args[2]))) args[2])))
parser.add_option( parser.add_option(
"-A", "--album", metavar='"ALBUM"', action="callback", "-A", "--album", metavar='"ALBUM"', action="callback",
help="Set the album title information", type="string", help="Set the album title information", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TALB"), callback=lambda *args: args[3].edits.append((fsnative(u"--TALB"),
args[2]))) args[2])))
parser.add_option( parser.add_option(
"-t", "--song", metavar='"SONG"', action="callback", "-t", "--song", metavar='"SONG"', action="callback",
help="Set the song title information", type="string", help="Set the song title information", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TIT2"), callback=lambda *args: args[3].edits.append((fsnative(u"--TIT2"),
args[2]))) args[2])))
parser.add_option( parser.add_option(
"-c", "--comment", metavar='"DESCRIPTION":"COMMENT":"LANGUAGE"', "-c", "--comment", metavar='"DESCRIPTION":"COMMENT":"LANGUAGE"',
action="callback", help="Set the comment information", type="string", action="callback", help="Set the comment information", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--COMM"), callback=lambda *args: args[3].edits.append((fsnative(u"--COMM"),
args[2]))) args[2])))
parser.add_option( parser.add_option(
"-p", "--picture", "-p", "--picture",
metavar='"FILENAME":"DESCRIPTION":"IMAGE-TYPE":"MIME-TYPE"', metavar='"FILENAME":"DESCRIPTION":"IMAGE-TYPE":"MIME-TYPE"',
action="callback", help="Set the picture", type="string", action="callback", help="Set the picture", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--APIC"), callback=lambda *args: args[3].edits.append((fsnative(u"--APIC"),
args[2]))) args[2])))
parser.add_option( parser.add_option(
"-g", "--genre", metavar='"GENRE"', action="callback", "-g", "--genre", metavar='"GENRE"', action="callback",
help="Set the genre or genre number", type="string", help="Set the genre or genre number", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TCON"), callback=lambda *args: args[3].edits.append((fsnative(u"--TCON"),
args[2]))) args[2])))
parser.add_option( parser.add_option(
"-y", "--year", "--date", metavar='YYYY[-MM-DD]', action="callback", "-y", "--year", "--date", metavar='YYYY[-MM-DD]', action="callback",
help="Set the year/date", type="string", help="Set the year/date", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TDRC"), callback=lambda *args: args[3].edits.append((fsnative(u"--TDRC"),
args[2]))) args[2])))
parser.add_option( parser.add_option(
"-T", "--track", metavar='"num/num"', action="callback", "-T", "--track", metavar='"num/num"', action="callback",
help="Set the track number/(optional) total tracks", type="string", help="Set the track number/(optional) total tracks", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TRCK"), callback=lambda *args: args[3].edits.append((fsnative(u"--TRCK"),
args[2]))) args[2])))
for key, frame in list(mutagen.id3.Frames.items()): for key, frame in mutagen.id3.Frames.items():
if (issubclass(frame, mutagen.id3.TextFrame) if (issubclass(frame, mutagen.id3.TextFrame)
or issubclass(frame, mutagen.id3.UrlFrame) or issubclass(frame, mutagen.id3.UrlFrame)
or issubclass(frame, mutagen.id3.POPM) or issubclass(frame, mutagen.id3.POPM)
or frame in (mutagen.id3.APIC, mutagen.id3.UFID)): or frame in (mutagen.id3.APIC, mutagen.id3.UFID,
mutagen.id3.USLT)):
parser.add_option( parser.add_option(
"--" + key, action="callback", help=SUPPRESS_HELP, "--" + key, action="callback", help=SUPPRESS_HELP,
type='string', metavar="value", # optparse blows up with this type='string', metavar="value", # optparse blows up with this
Executable → Regular
+2 -2
View File
@@ -46,7 +46,7 @@ def main(argv):
with _sig.block(): with _sig.block():
fileobjs = {} fileobjs = {}
format["base"] = os.path.splitext(os.path.basename(filename))[0] format["base"] = os.path.splitext(os.path.basename(filename))[0]
fileobj = open(filename, "rb") with open(filename, "rb") as fileobj:
if options.m3u: if options.m3u:
m3u = open(format["base"] + ".m3u", "w") m3u = open(format["base"] + ".m3u", "w")
fileobjs["m3u"] = m3u fileobjs["m3u"] = m3u
@@ -66,7 +66,7 @@ def main(argv):
if m3u: if m3u:
m3u.write(new_filename + "\r\n") m3u.write(new_filename + "\r\n")
fileobjs[page.serial].write(page.write()) fileobjs[page.serial].write(page.write())
for f in list(fileobjs.values()): for f in fileobjs.values():
f.close() f.close()
+5 -6
View File
@@ -9,7 +9,6 @@
"""Full tag list for any given file.""" """Full tag list for any given file."""
from mutagen._senf import print_, argv from mutagen._senf import print_, argv
from mutagen._compat import text_type
from ._util import SignalHandler, OptionParser from ._util import SignalHandler, OptionParser
@@ -30,14 +29,14 @@ def main(argv):
raise SystemExit(parser.print_help() or 1) raise SystemExit(parser.print_help() or 1)
for filename in args: for filename in args:
print_("--", filename) print_(u"--", filename)
try: try:
print_("-", File(filename).pprint()) print_(u"-", File(filename).pprint())
except AttributeError: except AttributeError:
print_("- Unknown file type") print_(u"- Unknown file type")
except Exception as err: except Exception as err:
print_(text_type(err)) print_(str(err))
print_("") print_(u"")
def entry_point(): def entry_point():
+2 -2
View File
@@ -83,7 +83,7 @@ def check_dir(path):
from mutagen.mp3 import MP3 from mutagen.mp3 import MP3
rep = Report(path) rep = Report(path)
print_("Scanning", path) print_(u"Scanning", path)
for path, dirs, files in os.walk(path): for path, dirs, files in os.walk(path):
files.sort() files.sort()
for fn in files: for fn in files:
@@ -105,7 +105,7 @@ def check_dir(path):
def main(argv): def main(argv):
if len(argv) == 1: if len(argv) == 1:
print_("Usage:", argv[0], "directory ...") print_(u"Usage:", argv[0], u"directory ...")
else: else:
for path in argv[1:]: for path in argv[1:]:
check_dir(path) check_dir(path)
Executable → Regular
+101 -137
View File
@@ -16,21 +16,48 @@ import sys
import struct import struct
import codecs import codecs
import errno import errno
import decimal
try: from io import BytesIO
import mmap
except ImportError:
# Google App Engine has no mmap:
# https://github.com/quodlibet/mutagen/issues/286
mmap = None
from collections import namedtuple from collections import namedtuple
from contextlib import contextmanager from contextlib import contextmanager
from functools import wraps from functools import wraps
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from ._compat import chr_, PY2, iteritems, iterbytes, integer_types, xrange, \
izip, text_type, reraise _DEFAULT_BUFFER_SIZE = 2 ** 18
def endswith(text, end):
# usefull for paths which can be both, str and bytes
if isinstance(text, str):
if not isinstance(end, str):
end = end.decode("ascii")
else:
if not isinstance(end, bytes):
end = end.encode("ascii")
return text.endswith(end)
def reraise(tp, value, tb):
raise tp(value).with_traceback(tb)
def bchr(x):
return bytes([x])
def iterbytes(b):
return (bytes([v]) for v in b)
def intround(value):
"""Given a float returns a rounded int. Should give the same result on
both Py2/3
"""
return int(decimal.Decimal.from_float(
value).to_integral_value(decimal.ROUND_HALF_EVEN))
def is_fileobj(fileobj): def is_fileobj(fileobj):
@@ -39,8 +66,8 @@ def is_fileobj(fileobj):
file object file object
""" """
# open() only handles str/bytes, so we can be strict return not (isinstance(fileobj, (str, bytes)) or
return not isinstance(fileobj, (text_type, bytes)) hasattr(fileobj, "__fspath__"))
def verify_fileobj(fileobj, writable=False): def verify_fileobj(fileobj, writable=False):
@@ -93,9 +120,9 @@ def fileobj_name(fileobj):
path type, but might be empty or non-existent. path type, but might be empty or non-existent.
""" """
value = getattr(fileobj, "name", "") value = getattr(fileobj, "name", u"")
if not isinstance(value, (text_type, bytes)): if not isinstance(value, (str, bytes)):
value = text_type(value) value = str(value)
return value return value
@@ -199,6 +226,10 @@ def _openfile(instance, filething, filename, fileobj, writable, create):
if filething is not None: if filething is not None:
if is_fileobj(filething): if is_fileobj(filething):
fileobj = filething fileobj = filething
elif hasattr(filething, "__fspath__"):
filename = filething.__fspath__()
if not isinstance(filename, (bytes, str)):
raise TypeError("expected __fspath__() to return a filename")
else: else:
filename = filething filename = filething
@@ -214,10 +245,24 @@ def _openfile(instance, filething, filename, fileobj, writable, create):
yield FileThing(fileobj, filename, filename or fileobj_name(fileobj)) yield FileThing(fileobj, filename, filename or fileobj_name(fileobj))
elif filename is not None: elif filename is not None:
verify_filename(filename) verify_filename(filename)
inmemory_fileobj = False
try: try:
fileobj = open(filename, "rb+" if writable else "rb") fileobj = open(filename, "rb+" if writable else "rb")
except IOError as e: except IOError as e:
if create and e.errno == errno.ENOENT: if writable and e.errno == errno.EOPNOTSUPP:
# Some file systems (gvfs over fuse) don't support opening
# files read/write. To make things still work read the whole
# file into an in-memory file like object and write it back
# later.
# https://github.com/quodlibet/mutagen/issues/300
try:
with open(filename, "rb") as fileobj:
fileobj = BytesIO(fileobj.read())
except IOError as e2:
raise MutagenError(e2)
inmemory_fileobj = True
elif create and e.errno == errno.ENOENT:
assert writable assert writable
try: try:
fileobj = open(filename, "wb+") fileobj = open(filename, "wb+")
@@ -228,6 +273,15 @@ def _openfile(instance, filething, filename, fileobj, writable, create):
with fileobj as fileobj: with fileobj as fileobj:
yield FileThing(fileobj, filename, filename) yield FileThing(fileobj, filename, filename)
if inmemory_fileobj:
assert writable
data = fileobj.getvalue()
try:
with open(filename, "wb") as fileobj:
fileobj.write(data)
except IOError as e:
raise MutagenError(e)
else: else:
raise TypeError("Missing filename or fileobj argument") raise TypeError("Missing filename or fileobj argument")
@@ -264,9 +318,6 @@ def hashable(cls):
Needs a working __eq__ and __hash__ and will add a __ne__. Needs a working __eq__ and __hash__ and will add a __ne__.
""" """
# py2
assert "__hash__" in cls.__dict__
# py3
assert cls.__dict__["__hash__"] is not None assert cls.__dict__["__hash__"] is not None
assert "__eq__" in cls.__dict__ assert "__eq__" in cls.__dict__
@@ -302,8 +353,8 @@ def enum(cls):
new_type.__module__ = cls.__module__ new_type.__module__ = cls.__module__
map_ = {} map_ = {}
for key, value in iteritems(d): for key, value in d.items():
if key.upper() == key and isinstance(value, integer_types): if key.upper() == key and isinstance(value, int):
value_instance = new_type(value) value_instance = new_type(value)
setattr(new_type, key, value_instance) setattr(new_type, key, value_instance)
map_[value] = key map_[value] = key
@@ -351,8 +402,8 @@ def flags(cls):
new_type.__module__ = cls.__module__ new_type.__module__ = cls.__module__
map_ = {} map_ = {}
for key, value in iteritems(d): for key, value in d.items():
if key.upper() == key and isinstance(value, integer_types): if key.upper() == key and isinstance(value, int):
value_instance = new_type(value) value_instance = new_type(value)
setattr(new_type, key, value_instance) setattr(new_type, key, value_instance)
map_[value] = key map_[value] = key
@@ -360,12 +411,12 @@ def flags(cls):
def str_(self): def str_(self):
value = int(self) value = int(self)
matches = [] matches = []
for k, v in list(map_.items()): for k, v in map_.items():
if value & k: if value & k:
matches.append("%s.%s" % (type(self).__name__, v)) matches.append("%s.%s" % (type(self).__name__, v))
value &= ~k value &= ~k
if value != 0 or not matches: if value != 0 or not matches:
matches.append(text_type(value)) matches.append(str(value))
return " | ".join(matches) return " | ".join(matches)
@@ -395,7 +446,7 @@ class DictMixin(object):
""" """
def __iter__(self): def __iter__(self):
return iter(list(self.keys())) return iter(self.keys())
def __has_key(self, key): def __has_key(self, key):
try: try:
@@ -405,25 +456,13 @@ class DictMixin(object):
else: else:
return True return True
if PY2:
has_key = __has_key
__contains__ = __has_key __contains__ = __has_key
if PY2:
iterkeys = lambda self: iter(list(self.keys()))
def values(self): def values(self):
return [self[k] for k in list(self.keys())] return [self[k] for k in self.keys()]
if PY2:
itervalues = lambda self: iter(list(self.values()))
def items(self): def items(self):
return list(zip(list(self.keys()), list(self.values()))) return list(zip(self.keys(), self.values()))
if PY2:
iteritems = lambda s: iter(list(s.items()))
def clear(self): def clear(self):
for key in list(self.keys()): for key in list(self.keys()):
@@ -443,7 +482,7 @@ class DictMixin(object):
return value return value
def popitem(self): def popitem(self):
for key in list(self.keys()): for key in self.keys():
break break
else: else:
raise KeyError("dictionary is empty") raise KeyError("dictionary is empty")
@@ -455,7 +494,7 @@ class DictMixin(object):
other = {} other = {}
try: try:
for key, value in list(other.items()): for key, value in other.items():
self.__setitem__(key, value) self.__setitem__(key, value)
except AttributeError: except AttributeError:
for key, value in other: for key, value in other:
@@ -475,18 +514,18 @@ class DictMixin(object):
return default return default
def __repr__(self): def __repr__(self):
return repr(dict(list(self.items()))) return repr(dict(self.items()))
def __eq__(self, other): def __eq__(self, other):
return dict(list(self.items())) == other return dict(self.items()) == other
def __lt__(self, other): def __lt__(self, other):
return dict(list(self.items())) < other return dict(self.items()) < other
__hash__ = object.__hash__ __hash__ = object.__hash__
def __len__(self): def __len__(self):
return len(list(self.keys())) return len(self.keys())
class DictProxy(DictMixin): class DictProxy(DictMixin):
@@ -504,7 +543,7 @@ class DictProxy(DictMixin):
del(self.__dict[key]) del(self.__dict[key])
def keys(self): def keys(self):
return list(self.__dict.keys()) return self.__dict.keys()
def _fill_cdata(cls): def _fill_cdata(cls):
@@ -553,7 +592,7 @@ def _fill_cdata(cls):
funcs["to_%s%s%s" % (prefix, name, esuffix)] = pack funcs["to_%s%s%s" % (prefix, name, esuffix)] = pack
funcs["to_%sint%s%s" % (prefix, bits, esuffix)] = pack funcs["to_%sint%s%s" % (prefix, bits, esuffix)] = pack
for key, func in iteritems(funcs): for key, func in funcs.items():
setattr(cls, key, staticmethod(func)) setattr(cls, key, staticmethod(func))
@@ -564,11 +603,10 @@ class cdata(object):
uint32_le(data)/to_uint32_le(num)/uint32_le_from(data, offset=0) uint32_le(data)/to_uint32_le(num)/uint32_le_from(data, offset=0)
""" """
from struct import error error = struct.error
error = error
bitswap = b''.join( bitswap = b''.join(
chr_(sum(((val >> i) & 1) << (7 - i) for i in range(8))) bchr(sum(((val >> i) & 1) << (7 - i) for i in range(8)))
for val in range(256)) for val in range(256))
test_bit = staticmethod(lambda value, n: bool((value >> n) & 1)) test_bit = staticmethod(lambda value, n: bool((value >> n) & 1))
@@ -598,7 +636,7 @@ def get_size(fileobj):
def read_full(fileobj, size): def read_full(fileobj, size):
"""Like fileobj.read but raises IOError if no all requested data is """Like fileobj.read but raises IOError if not all requested data is
returned. returned.
If you want to distinguish IOError and the EOS case, better handle If you want to distinguish IOError and the EOS case, better handle
@@ -645,65 +683,7 @@ def seek_end(fileobj, offset):
fileobj.seek(-offset, 2) fileobj.seek(-offset, 2)
def mmap_move(fileobj, dest, src, count): def resize_file(fobj, diff, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
"""Mmaps the file object if possible and moves 'count' data
from 'src' to 'dest'. All data has to be inside the file size
(enlarging the file through this function isn't possible)
Will adjust the file offset.
Args:
fileobj (fileobj)
dest (int): The destination offset
src (int): The source offset
count (int) The amount of data to move
Raises:
mmap.error: In case move failed
IOError: In case an operation on the fileobj fails
ValueError: In case invalid parameters were given
"""
assert mmap is not None, "no mmap support"
if dest < 0 or src < 0 or count < 0:
raise ValueError("Invalid parameters")
try:
fileno = fileobj.fileno()
except (AttributeError, IOError):
raise mmap.error(
"File object does not expose/support a file descriptor")
fileobj.seek(0, 2)
filesize = fileobj.tell()
length = max(dest, src) + count
if length > filesize:
raise ValueError("Not in file size boundary")
offset = ((min(dest, src) // mmap.ALLOCATIONGRANULARITY) *
mmap.ALLOCATIONGRANULARITY)
assert dest >= offset
assert src >= offset
assert offset % mmap.ALLOCATIONGRANULARITY == 0
# Windows doesn't handle empty mappings, add a fast path here instead
if count == 0:
return
# fast path
if src == dest:
return
fileobj.flush()
file_map = mmap.mmap(fileno, length - offset, offset=offset)
try:
file_map.move(dest - offset, src - offset, count)
finally:
file_map.close()
def resize_file(fobj, diff, BUFFER_SIZE=2 ** 16):
"""Resize a file by `diff`. """Resize a file by `diff`.
New space will be filled with zeros. New space will be filled with zeros.
@@ -740,7 +720,7 @@ def resize_file(fobj, diff, BUFFER_SIZE=2 ** 16):
raise raise
def fallback_move(fobj, dest, src, count, BUFFER_SIZE=2 ** 16): def move_bytes(fobj, dest, src, count, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
"""Moves data around using read()/write(). """Moves data around using read()/write().
Args: Args:
@@ -783,12 +763,11 @@ def fallback_move(fobj, dest, src, count, BUFFER_SIZE=2 ** 16):
fobj.flush() fobj.flush()
def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): def insert_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
"""Insert size bytes of empty space starting at offset. """Insert size bytes of empty space starting at offset.
fobj must be an open file object, open rb+ or fobj must be an open file object, open rb+ or
equivalent. Mutagen tries to use mmap to resize the file, but equivalent.
falls back to a significantly slower method if mmap fails.
Args: Args:
fobj (fileobj) fobj (fileobj)
@@ -809,22 +788,14 @@ def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16):
raise ValueError raise ValueError
resize_file(fobj, size, BUFFER_SIZE) resize_file(fobj, size, BUFFER_SIZE)
move_bytes(fobj, offset + size, offset, movesize, BUFFER_SIZE)
if mmap is not None:
try:
mmap_move(fobj, offset + size, offset, movesize)
except mmap.error:
fallback_move(fobj, offset + size, offset, movesize, BUFFER_SIZE)
else:
fallback_move(fobj, offset + size, offset, movesize, BUFFER_SIZE)
def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): def delete_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
"""Delete size bytes of empty space starting at offset. """Delete size bytes of empty space starting at offset.
fobj must be an open file object, open rb+ or fobj must be an open file object, open rb+ or
equivalent. Mutagen tries to use mmap to resize the file, but equivalent.
falls back to a significantly slower method if mmap fails.
Args: Args:
fobj (fileobj) fobj (fileobj)
@@ -844,14 +815,7 @@ def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16):
if movesize < 0: if movesize < 0:
raise ValueError raise ValueError
if mmap is not None: move_bytes(fobj, offset, offset + size, movesize, BUFFER_SIZE)
try:
mmap_move(fobj, offset, offset + size, movesize)
except mmap.error:
fallback_move(fobj, offset, offset + size, movesize, BUFFER_SIZE)
else:
fallback_move(fobj, offset, offset + size, movesize, BUFFER_SIZE)
resize_file(fobj, -size, BUFFER_SIZE) resize_file(fobj, -size, BUFFER_SIZE)
@@ -895,7 +859,7 @@ def dict_match(d, key, default=None):
if key in d and "[" not in key: if key in d and "[" not in key:
return d[key] return d[key]
else: else:
for pattern, value in iteritems(d): for pattern, value in d.items():
if fnmatchcase(key, pattern): if fnmatchcase(key, pattern):
return value return value
return default return default
@@ -976,15 +940,15 @@ def decode_terminated(data, encoding, strict=True):
r = [] r = []
for i, b in enumerate(iterbytes(data)): for i, b in enumerate(iterbytes(data)):
c = decoder.decode(b) c = decoder.decode(b)
if c == "\x00": if c == u"\x00":
return "".join(r), data[i + 1:] return u"".join(r), data[i + 1:]
r.append(c) r.append(c)
else: else:
# make sure the decoder is finished # make sure the decoder is finished
r.append(decoder.decode(b"", True)) r.append(decoder.decode(b"", True))
if strict: if strict:
raise ValueError("not null terminated") raise ValueError("not null terminated")
return "".join(r), b"" return u"".join(r), b""
class BitReaderError(Exception): class BitReaderError(Exception):
Executable → Regular
+14 -33
View File
@@ -17,10 +17,10 @@ The specification is at http://www.xiph.org/vorbis/doc/v-comment.html.
""" """
import sys import sys
from io import BytesIO
import mutagen import mutagen
from ._compat import reraise, BytesIO, text_type, xrange, PY3, PY2 from mutagen._util import DictMixin, cdata, MutagenError, reraise
from mutagen._util import DictMixin, cdata, MutagenError
def is_valid_key(key): def is_valid_key(key):
@@ -32,7 +32,7 @@ def is_valid_key(key):
Takes str/unicode in Python 2, unicode in Python 3 Takes str/unicode in Python 2, unicode in Python 3
""" """
if PY3 and isinstance(key, bytes): if isinstance(key, bytes):
raise TypeError("needs to be str not bytes") raise TypeError("needs to be str not bytes")
for c in key: for c in key:
@@ -71,7 +71,7 @@ class VComment(mutagen.Tags, list):
vendor (text): the stream 'vendor' (i.e. writer); default 'Mutagen' vendor (text): the stream 'vendor' (i.e. writer); default 'Mutagen'
""" """
vendor = "Mutagen " + mutagen.version_string vendor = u"Mutagen " + mutagen.version_string
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
self._size = 0 self._size = 0
@@ -116,7 +116,7 @@ class VComment(mutagen.Tags, list):
if errors == "ignore": if errors == "ignore":
continue continue
elif errors == "replace": elif errors == "replace":
tag, value = "unknown%d" % i, string tag, value = u"unknown%d" % i, string
else: else:
reraise(VorbisEncodingError, err, sys.exc_info()[2]) reraise(VorbisEncodingError, err, sys.exc_info()[2])
try: try:
@@ -124,8 +124,6 @@ class VComment(mutagen.Tags, list):
except UnicodeEncodeError: except UnicodeEncodeError:
raise VorbisEncodingError("invalid tag name %r" % tag) raise VorbisEncodingError("invalid tag name %r" % tag)
else: else:
# string keys in py3k
if PY3:
tag = tag.decode("ascii") tag = tag.decode("ascii")
if is_valid_key(tag): if is_valid_key(tag):
self.append((tag, value)) self.append((tag, value))
@@ -145,30 +143,19 @@ class VComment(mutagen.Tags, list):
In Python 3 all keys and values have to be a string. In Python 3 all keys and values have to be a string.
""" """
if not isinstance(self.vendor, text_type): if not isinstance(self.vendor, str):
if PY3:
raise ValueError("vendor needs to be str") raise ValueError("vendor needs to be str")
try:
self.vendor.decode('utf-8')
except UnicodeDecodeError:
raise ValueError
for key, value in self: for key, value in self:
try: try:
if not is_valid_key(key): if not is_valid_key(key):
raise ValueError raise ValueError("%r is not a valid key" % key)
except TypeError: except TypeError:
raise ValueError("%r is not a valid key" % key) raise ValueError("%r is not a valid key" % key)
if not isinstance(value, text_type): if not isinstance(value, str):
if PY3: err = "%r needs to be str for key %r" % (value, key)
raise ValueError("%r needs to be str" % key) raise ValueError(err)
try:
value.decode("utf-8")
except:
raise ValueError("%r is not a valid value" % value)
return True return True
@@ -213,12 +200,12 @@ class VComment(mutagen.Tags, list):
def pprint(self): def pprint(self):
def _decode(value): def _decode(value):
if not isinstance(value, text_type): if not isinstance(value, str):
return value.decode('utf-8', 'replace') return value.decode('utf-8', 'replace')
return value return value
tags = ["%s=%s" % (_decode(k), _decode(v)) for k, v in self] tags = [u"%s=%s" % (_decode(k), _decode(v)) for k, v in self]
return "\n".join(tags) return u"\n".join(tags)
class VCommentDict(VComment, DictMixin): class VCommentDict(VComment, DictMixin):
@@ -242,7 +229,6 @@ class VCommentDict(VComment, DictMixin):
work. work.
""" """
# PY3 only
if isinstance(key, slice): if isinstance(key, slice):
return VComment.__getitem__(self, key) return VComment.__getitem__(self, key)
@@ -260,7 +246,6 @@ class VCommentDict(VComment, DictMixin):
def __delitem__(self, key): def __delitem__(self, key):
"""Delete all values associated with the key.""" """Delete all values associated with the key."""
# PY3 only
if isinstance(key, slice): if isinstance(key, slice):
return VComment.__delitem__(self, key) return VComment.__delitem__(self, key)
@@ -296,7 +281,6 @@ class VCommentDict(VComment, DictMixin):
string. string.
""" """
# PY3 only
if isinstance(key, slice): if isinstance(key, slice):
return VComment.__setitem__(self, key, values) return VComment.__setitem__(self, key, values)
@@ -310,9 +294,6 @@ class VCommentDict(VComment, DictMixin):
except KeyError: except KeyError:
pass pass
if PY2:
key = key.encode('ascii')
for value in values: for value in values:
self.append((key, value)) self.append((key, value))
@@ -324,4 +305,4 @@ class VCommentDict(VComment, DictMixin):
def as_dict(self): def as_dict(self):
"""Return a copy of the comment data in a real dict.""" """Return a copy of the comment data in a real dict."""
return dict([(key, self[key]) for key in list(self.keys())]) return dict([(key, self[key]) for key in self.keys()])
Executable → Regular
+6 -4
View File
@@ -15,9 +15,8 @@
from mutagen import StreamInfo from mutagen import StreamInfo
from mutagen._file import FileType from mutagen._file import FileType
from mutagen._util import BitReader, BitReaderError, MutagenError, loadfile, \ from mutagen._util import BitReader, BitReaderError, MutagenError, loadfile, \
convert_error convert_error, endswith
from mutagen.id3._util import BitPaddedInt from mutagen.id3._util import BitPaddedInt
from mutagen._compat import endswith, xrange
_FREQS = [ _FREQS = [
@@ -375,10 +374,13 @@ class AACInfo(StreamInfo):
fileobj.seek(0, 2) fileobj.seek(0, 2)
stream_size = fileobj.tell() - (offset + s.offset) stream_size = fileobj.tell() - (offset + s.offset)
# approx # approx
self.length = float(s.samples * stream_size) / (s.size * s.frequency) self.length = 0.0
if s.frequency != 0:
self.length = \
float(s.samples * stream_size) / (s.size * s.frequency)
def pprint(self): def pprint(self):
return "AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % ( return u"AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % (
self._type, self.sample_rate, self.length, self.channels, self._type, self.sample_rate, self.length, self.channels,
self.bitrate) self.bitrate)
+330
View File
@@ -0,0 +1,330 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2019 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""Pure AC3 file information.
"""
__all__ = ["AC3", "Open"]
from mutagen import StreamInfo
from mutagen._file import FileType
from mutagen._util import (
BitReader,
BitReaderError,
MutagenError,
convert_error,
enum,
loadfile,
endswith,
)
@enum
class ChannelMode(object):
DUALMONO = 0
MONO = 1
STEREO = 2
C3F = 3
C2F1R = 4
C3F1R = 5
C2F2R = 6
C3F2R = 7
AC3_CHANNELS = {
ChannelMode.DUALMONO: 2,
ChannelMode.MONO: 1,
ChannelMode.STEREO: 2,
ChannelMode.C3F: 3,
ChannelMode.C2F1R: 3,
ChannelMode.C3F1R: 4,
ChannelMode.C2F2R: 4,
ChannelMode.C3F2R: 5
}
AC3_HEADER_SIZE = 7
AC3_SAMPLE_RATES = [48000, 44100, 32000]
AC3_BITRATES = [
32, 40, 48, 56, 64, 80, 96, 112, 128,
160, 192, 224, 256, 320, 384, 448, 512, 576, 640
]
@enum
class EAC3FrameType(object):
INDEPENDENT = 0
DEPENDENT = 1
AC3_CONVERT = 2
RESERVED = 3
EAC3_BLOCKS = [1, 2, 3, 6]
class AC3Error(MutagenError):
pass
class AC3Info(StreamInfo):
"""AC3 stream information.
The length of the stream is just a guess and might not be correct.
Attributes:
channels (`int`): number of audio channels
length (`float`): file length in seconds, as a float
sample_rate (`int`): audio sampling rate in Hz
bitrate (`int`): audio bitrate, in bits per second
codec (`str`): ac-3 or ec-3 (Enhanced AC-3)
"""
channels = 0
length = 0
sample_rate = 0
bitrate = 0
codec = 'ac-3'
@convert_error(IOError, AC3Error)
def __init__(self, fileobj):
"""Raises AC3Error"""
header = bytearray(fileobj.read(6))
if len(header) < 6:
raise AC3Error("not enough data")
if not header.startswith(b"\x0b\x77"):
raise AC3Error("not a AC3 file")
bitstream_id = header[5] >> 3
if bitstream_id > 16:
raise AC3Error("invalid bitstream_id %i" % bitstream_id)
fileobj.seek(2)
self._read_header(fileobj, bitstream_id)
def _read_header(self, fileobj, bitstream_id):
bitreader = BitReader(fileobj)
try:
# This is partially based on code from
# https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/ac3_parser.c
if bitstream_id <= 10: # Normal AC-3
self._read_header_normal(bitreader, bitstream_id)
else: # Enhanced AC-3
self._read_header_enhanced(bitreader)
except BitReaderError as e:
raise AC3Error(e)
self.length = self._guess_length(fileobj)
def _read_header_normal(self, bitreader, bitstream_id):
r = bitreader
r.skip(16) # 16 bit CRC
sr_code = r.bits(2)
if sr_code == 3:
raise AC3Error("invalid sample rate code %i" % sr_code)
frame_size_code = r.bits(6)
if frame_size_code > 37:
raise AC3Error("invalid frame size code %i" % frame_size_code)
r.skip(5) # bitstream ID, already read
r.skip(3) # bitstream mode, not needed
channel_mode = ChannelMode(r.bits(3))
r.skip(2) # dolby surround mode or surround mix level
lfe_on = r.bits(1)
sr_shift = max(bitstream_id, 8) - 8
try:
self.sample_rate = AC3_SAMPLE_RATES[sr_code] >> sr_shift
self.bitrate = (AC3_BITRATES[frame_size_code >> 1] * 1000
) >> sr_shift
except KeyError as e:
raise AC3Error(e)
self.channels = self._get_channels(channel_mode, lfe_on)
self._skip_unused_header_bits_normal(r, channel_mode)
def _read_header_enhanced(self, bitreader):
r = bitreader
self.codec = "ec-3"
frame_type = r.bits(2)
if frame_type == EAC3FrameType.RESERVED:
raise AC3Error("invalid frame type %i" % frame_type)
r.skip(3) # substream ID, not needed
frame_size = (r.bits(11) + 1) << 1
if frame_size < AC3_HEADER_SIZE:
raise AC3Error("invalid frame size %i" % frame_size)
sr_code = r.bits(2)
try:
if sr_code == 3:
sr_code2 = r.bits(2)
if sr_code2 == 3:
raise AC3Error("invalid sample rate code %i" % sr_code2)
numblocks_code = 3
self.sample_rate = AC3_SAMPLE_RATES[sr_code2] // 2
else:
numblocks_code = r.bits(2)
self.sample_rate = AC3_SAMPLE_RATES[sr_code]
channel_mode = ChannelMode(r.bits(3))
lfe_on = r.bits(1)
self.bitrate = 8 * frame_size * self.sample_rate // (
EAC3_BLOCKS[numblocks_code] * 256)
except KeyError as e:
raise AC3Error(e)
r.skip(5) # bitstream ID, already read
self.channels = self._get_channels(channel_mode, lfe_on)
self._skip_unused_header_bits_enhanced(
r, frame_type, channel_mode, sr_code, numblocks_code)
@staticmethod
def _skip_unused_header_bits_normal(bitreader, channel_mode):
r = bitreader
r.skip(5) # Dialogue Normalization
if r.bits(1): # Compression Gain Word Exists
r.skip(8) # Compression Gain Word
if r.bits(1): # Language Code Exists
r.skip(8) # Language Code
if r.bits(1): # Audio Production Information Exists
# Mixing Level, 5 Bits
# Room Type, 2 Bits
r.skip(7)
if channel_mode == ChannelMode.DUALMONO:
r.skip(5) # Dialogue Normalization, ch2
if r.bits(1): # Compression Gain Word Exists, ch2
r.skip(8) # Compression Gain Word, ch2
if r.bits(1): # Language Code Exists, ch2
r.skip(8) # Language Code, ch2
if r.bits(1): # Audio Production Information Exists, ch2
# Mixing Level, ch2, 5 Bits
# Room Type, ch2, 2 Bits
r.skip(7)
# Copyright Bit, 1 Bit
# Original Bit Stream, 1 Bit
r.skip(2)
timecod1e = r.bits(1) # Time Code First Halve Exists
timecod2e = r.bits(1) # Time Code Second Halve Exists
if timecod1e:
r.skip(14) # Time Code First Half
if timecod2e:
r.skip(14) # Time Code Second Half
if r.bits(1): # Additional Bit Stream Information Exists
addbsil = r.bit(6) # Additional Bit Stream Information Length
r.skip((addbsil + 1) * 8)
@staticmethod
def _skip_unused_header_bits_enhanced(bitreader, frame_type, channel_mode,
sr_code, numblocks_code):
r = bitreader
r.skip(5) # Dialogue Normalization
if r.bits(1): # Compression Gain Word Exists
r.skip(8) # Compression Gain Word
if channel_mode == ChannelMode.DUALMONO:
r.skip(5) # Dialogue Normalization, ch2
if r.bits(1): # Compression Gain Word Exists, ch2
r.skip(8) # Compression Gain Word, ch2
if frame_type == EAC3FrameType.DEPENDENT:
if r.bits(1): # chanmap exists
r.skip(16) # chanmap
if r.bits(1): # mixmdate, 1 Bit
# FIXME: Handle channel dependent fields
return
if r.bits(1): # Informational Metadata Exists
# bsmod, 3 Bits
# Copyright Bit, 1 Bit
# Original Bit Stream, 1 Bit
r.skip(5)
if channel_mode == ChannelMode.STEREO:
# dsurmod. 2 Bits
# dheadphonmod, 2 Bits
r.skip(4)
elif channel_mode >= ChannelMode.C2F2R:
r.skip(2) # dsurexmod
if r.bits(1): # Audio Production Information Exists
# Mixing Level, 5 Bits
# Room Type, 2 Bits
# adconvtyp, 1 Bit
r.skip(8)
if channel_mode == ChannelMode.DUALMONO:
if r.bits(1): # Audio Production Information Exists, ch2
# Mixing Level, ch2, 5 Bits
# Room Type, ch2, 2 Bits
# adconvtyp, ch2, 1 Bit
r.skip(8)
if sr_code < 3: # if not half sample rate
r.skip(1) # sourcefscod
if frame_type == EAC3FrameType.INDEPENDENT and numblocks_code == 3:
r.skip(1) # convsync
if frame_type == EAC3FrameType.AC3_CONVERT:
if numblocks_code != 3:
if r.bits(1): # blkid
r.skip(6) # frmsizecod
if r.bits(1): # Additional Bit Stream Information Exists
addbsil = r.bit(6) # Additional Bit Stream Information Length
r.skip((addbsil + 1) * 8)
@staticmethod
def _get_channels(channel_mode, lfe_on):
try:
return AC3_CHANNELS[channel_mode] + lfe_on
except KeyError as e:
raise AC3Error(e)
def _guess_length(self, fileobj):
# use bitrate + data size to guess length
if self.bitrate == 0:
return
start = fileobj.tell()
fileobj.seek(0, 2)
length = fileobj.tell() - start
return 8.0 * length / self.bitrate
def pprint(self):
return u"%s, %d Hz, %.2f seconds, %d channel(s), %d bps" % (
self.codec, self.sample_rate, self.length, self.channels,
self.bitrate)
class AC3(FileType):
"""AC3(filething)
Arguments:
filething (filething)
Load AC3 or EAC3 files.
Tagging is not supported.
Use the ID3/APEv2 classes directly instead.
Attributes:
info (`AC3Info`)
"""
_mimes = ["audio/ac3"]
@loadfile()
def load(self, filething):
self.info = AC3Info(filething.fileobj)
def add_tags(self):
raise AC3Error("doesn't support tags")
@staticmethod
def score(filename, fileobj, header):
return header.startswith(b"\x0b\x77") * 2 \
+ (endswith(filename, ".ac3") or endswith(filename, ".eac3"))
Open = AC3
error = AC3Error
Executable → Regular
+79 -207
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (C) 2014 Evan Purkhiser # Copyright (C) 2014 Evan Purkhiser
# 2014 Ben Ockmore # 2014 Ben Ockmore
# 2019-2020 Philipp Wolfer
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -9,26 +10,30 @@
"""AIFF audio stream information and tags.""" """AIFF audio stream information and tags."""
import sys
import struct import struct
from struct import pack from struct import pack
from ._compat import endswith, text_type, reraise
from mutagen import StreamInfo, FileType from mutagen import StreamInfo, FileType
from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import resize_bytes, delete_bytes, MutagenError, loadfile, \ from mutagen._iff import (
convert_error IffChunk,
IffContainerChunkMixin,
IffFile,
IffID3,
InvalidChunk,
error as IffError,
)
from mutagen._util import (
convert_error,
loadfile,
endswith,
)
__all__ = ["AIFF", "Open", "delete"] __all__ = ["AIFF", "Open", "delete"]
class error(MutagenError): class error(IffError):
pass
class InvalidChunk(error):
pass pass
@@ -36,14 +41,10 @@ class InvalidChunk(error):
_HUGE_VAL = 1.79769313486231e+308 _HUGE_VAL = 1.79769313486231e+308
def is_valid_chunk_id(id): def read_float(data):
assert isinstance(id, text_type) """Raises OverflowError"""
return ((len(id) <= 4) and (min(id) >= ' ') and assert len(data) == 10
(max(id) <= '~'))
def read_float(data): # 10 bytes
expon, himant, lomant = struct.unpack('>hLL', data) expon, himant, lomant = struct.unpack('>hLL', data)
sign = 1 sign = 1
if expon < 0: if expon < 0:
@@ -52,168 +53,70 @@ def read_float(data): # 10 bytes
if expon == himant == lomant == 0: if expon == himant == lomant == 0:
f = 0.0 f = 0.0
elif expon == 0x7FFF: elif expon == 0x7FFF:
f = _HUGE_VAL raise OverflowError("inf and nan not supported")
else: else:
expon = expon - 16383 expon = expon - 16383
# this can raise OverflowError too
f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63) f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63)
return sign * f return sign * f
class IFFChunk(object): class AIFFChunk(IffChunk):
"""Representation of a single IFF chunk""" """Representation of a single IFF chunk"""
# Chunk headers are 8 bytes long (4 for ID and 4 for the size) @classmethod
HEADER_SIZE = 8 def parse_header(cls, header):
return struct.unpack('>4sI', header)
def __init__(self, fileobj, parent_chunk=None): @classmethod
self.__fileobj = fileobj def get_class(cls, id):
self.parent_chunk = parent_chunk if id == 'FORM':
self.offset = fileobj.tell() return AIFFFormChunk
else:
return cls
header = fileobj.read(self.HEADER_SIZE) def write_new_header(self, id_, size):
if len(header) < self.HEADER_SIZE: self._fileobj.write(pack('>4sI', id_, size))
raise InvalidChunk()
self.id, self.data_size = struct.unpack('>4si', header) def write_size(self):
self._fileobj.write(pack('>I', self.data_size))
try:
self.id = self.id.decode('ascii')
except UnicodeDecodeError:
raise InvalidChunk()
if not is_valid_chunk_id(self.id):
raise InvalidChunk()
self.size = self.HEADER_SIZE + self.data_size
self.data_offset = fileobj.tell()
def read(self):
"""Read the chunks data"""
self.__fileobj.seek(self.data_offset)
return self.__fileobj.read(self.data_size)
def write(self, data):
"""Write the chunk data"""
if len(data) > self.data_size:
raise ValueError
self.__fileobj.seek(self.data_offset)
self.__fileobj.write(data)
def delete(self):
"""Removes the chunk from the file"""
delete_bytes(self.__fileobj, self.size, self.offset)
if self.parent_chunk is not None:
self.parent_chunk._update_size(
self.parent_chunk.data_size - self.size)
def _update_size(self, data_size):
"""Update the size of the chunk"""
self.__fileobj.seek(self.offset + 4)
self.__fileobj.write(pack('>I', data_size))
if self.parent_chunk is not None:
size_diff = self.data_size - data_size
self.parent_chunk._update_size(
self.parent_chunk.data_size - size_diff)
self.data_size = data_size
self.size = data_size + self.HEADER_SIZE
def resize(self, new_data_size):
"""Resize the file and update the chunk sizes"""
resize_bytes(
self.__fileobj, self.data_size, new_data_size, self.data_offset)
self._update_size(new_data_size)
class IFFFile(object): class AIFFFormChunk(AIFFChunk, IffContainerChunkMixin):
"""Representation of a IFF file""" """The AIFF root chunk."""
def parse_next_subchunk(self):
return AIFFChunk.parse(self._fileobj, self)
def __init__(self, fileobj, id, data_size, parent_chunk):
if id != u'FORM':
raise InvalidChunk('Expected FORM chunk, got %s' % id)
AIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk)
self.init_container()
class AIFFFile(IffFile):
"""Representation of a AIFF file"""
def __init__(self, fileobj): def __init__(self, fileobj):
self.__fileobj = fileobj
self.__chunks = {}
# AIFF Files always start with the FORM chunk which contains a 4 byte # AIFF Files always start with the FORM chunk which contains a 4 byte
# ID before the start of other chunks # ID before the start of other chunks
fileobj.seek(0) super().__init__(AIFFChunk, fileobj)
self.__chunks['FORM'] = IFFChunk(fileobj)
# Skip past the 4 byte FORM id if self.root.id != u'FORM':
fileobj.seek(IFFChunk.HEADER_SIZE + 4) raise InvalidChunk("Root chunk must be a FORM chunk, got %s"
% self.root.id)
# Where the next chunk can be located. We need to keep track of this
# since the size indicated in the FORM header may not match up with the
# offset determined from the size of the last chunk in the file
self.__next_offset = fileobj.tell()
# Load all of the chunks
while True:
try:
chunk = IFFChunk(fileobj, self['FORM'])
except InvalidChunk:
break
self.__chunks[chunk.id.strip()] = chunk
# Calculate the location of the next chunk,
# considering the pad byte
self.__next_offset = chunk.offset + chunk.size
self.__next_offset += self.__next_offset % 2
fileobj.seek(self.__next_offset)
def __contains__(self, id_): def __contains__(self, id_):
"""Check if the IFF file contains a specific chunk""" if id_ == 'FORM': # For backwards compatibility
return True
assert isinstance(id_, text_type) return super().__contains__(id_)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
return id_ in self.__chunks
def __getitem__(self, id_): def __getitem__(self, id_):
"""Get a chunk from the IFF file""" if id_ == 'FORM': # For backwards compatibility
return self.root
assert isinstance(id_, text_type) return super().__getitem__(id_)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
try:
return self.__chunks[id_]
except KeyError:
raise KeyError(
"%r has no %r chunk" % (self.__fileobj, id_))
def __delitem__(self, id_):
"""Remove a chunk from the IFF file"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
self.__chunks.pop(id_).delete()
def insert_chunk(self, id_):
"""Insert a new chunk at the end of the IFF file"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
self.__fileobj.seek(self.__next_offset)
self.__fileobj.write(pack('>4si', id_.ljust(4).encode('ascii'), 0))
self.__fileobj.seek(self.__next_offset)
chunk = IFFChunk(self.__fileobj, self['FORM'])
self['FORM']._update_size(self['FORM'].data_size + chunk.size)
self.__chunks[id_] = chunk
self.__next_offset = chunk.offset + chunk.size
class AIFFInfo(StreamInfo): class AIFFInfo(StreamInfo):
@@ -228,7 +131,7 @@ class AIFFInfo(StreamInfo):
bitrate (`int`): audio bitrate, in bits per second bitrate (`int`): audio bitrate, in bits per second
channels (`int`): The number of audio channels channels (`int`): The number of audio channels
sample_rate (`int`): audio sample rate, in Hz sample_rate (`int`): audio sample rate, in Hz
sample_size (`int`): The audio sample size bits_per_sample (`int`): The audio sample size
""" """
length = 0 length = 0
@@ -240,9 +143,9 @@ class AIFFInfo(StreamInfo):
def __init__(self, fileobj): def __init__(self, fileobj):
"""Raises error""" """Raises error"""
iff = IFFFile(fileobj) iff = AIFFFile(fileobj)
try: try:
common_chunk = iff['COMM'] common_chunk = iff[u'COMM']
except KeyError as e: except KeyError as e:
raise error(str(e)) raise error(str(e))
@@ -253,61 +156,30 @@ class AIFFInfo(StreamInfo):
info = struct.unpack('>hLh10s', data[:18]) info = struct.unpack('>hLh10s', data[:18])
channels, frame_count, sample_size, sample_rate = info channels, frame_count, sample_size, sample_rate = info
try:
self.sample_rate = int(read_float(sample_rate)) self.sample_rate = int(read_float(sample_rate))
self.sample_size = sample_size except OverflowError:
self.channels = channels raise error("Invalid sample rate")
self.bitrate = channels * sample_size * self.sample_rate if self.sample_rate < 0:
raise error("Invalid sample rate")
if self.sample_rate != 0:
self.length = frame_count / float(self.sample_rate) self.length = frame_count / float(self.sample_rate)
self.bits_per_sample = sample_size
self.sample_size = sample_size # For backward compatibility
self.channels = channels
self.bitrate = channels * sample_size * self.sample_rate
def pprint(self): def pprint(self):
return "%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % ( return u"%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % (
self.channels, self.bitrate, self.sample_rate, self.length) self.channels, self.bitrate, self.sample_rate, self.length)
class _IFFID3(ID3): class _IFFID3(IffID3):
"""A AIFF file with ID3v2 tags""" """A AIFF file with ID3v2 tags"""
def _pre_load_header(self, fileobj): def _load_file(self, fileobj):
try: return AIFFFile(fileobj)
fileobj.seek(IFFFile(fileobj)['ID3'].data_offset)
except (InvalidChunk, KeyError):
raise ID3NoHeaderError("No ID3 chunk")
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the AIFF file"""
fileobj = filething.fileobj
iff_file = IFFFile(fileobj)
if 'ID3' not in iff_file:
iff_file.insert_chunk('ID3')
chunk = iff_file['ID3']
try:
data = self._prepare_data(
fileobj, chunk.data_offset, chunk.data_size, v2_version,
v23_sep, padding)
except ID3Error as e:
reraise(error, e, sys.exc_info()[2])
new_size = len(data)
new_size += new_size % 2 # pad byte
assert new_size % 2 == 0
chunk.resize(new_size)
data += (new_size - len(data)) * b'\x00'
assert new_size == len(data)
chunk.write(data)
@loadfile(writable=True)
def delete(self, filething):
"""Completely removes the ID3 chunk from the AIFF file"""
delete(filething)
self.clear()
@convert_error(IOError, error) @convert_error(IOError, error)
@@ -316,7 +188,7 @@ def delete(filething):
"""Completely removes the ID3 chunk from the AIFF file""" """Completely removes the ID3 chunk from the AIFF file"""
try: try:
del IFFFile(filething.fileobj)['ID3'] del AIFFFile(filething.fileobj)[u'ID3']
except KeyError: except KeyError:
pass pass
Executable → Regular
+30 -68
View File
@@ -32,29 +32,22 @@ __all__ = ["APEv2", "APEv2File", "Open", "delete"]
import sys import sys
import struct import struct
from collections import MutableSequence from io import BytesIO
from collections.abc import MutableSequence
from ._compat import (cBytesIO, PY3, text_type, PY2, reraise, swap_to_string,
xrange)
from mutagen import Metadata, FileType, StreamInfo from mutagen import Metadata, FileType, StreamInfo
from mutagen._util import DictMixin, cdata, delete_bytes, total_ordering, \ from mutagen._util import DictMixin, cdata, delete_bytes, total_ordering, \
MutagenError, loadfile, convert_error, seek_end, get_size MutagenError, loadfile, convert_error, seek_end, get_size, reraise
def is_valid_apev2_key(key): def is_valid_apev2_key(key):
if not isinstance(key, text_type): if not isinstance(key, str):
if PY3:
raise TypeError("APEv2 key must be str") raise TypeError("APEv2 key must be str")
try:
key = key.decode('ascii')
except UnicodeDecodeError:
return False
# PY26 - Change to set literal syntax (since set is faster than list here) # PY26 - Change to set literal syntax (since set is faster than list here)
return ((2 <= len(key) <= 255) and (min(key) >= ' ') and return ((2 <= len(key) <= 255) and (min(key) >= u' ') and
(max(key) <= '~') and (max(key) <= u'~') and
(key not in ["OggS", "TAG", "ID3", "MP+"])) (key not in [u"OggS", u"TAG", u"ID3", u"MP+"]))
# There are three different kinds of APE tag values. # There are three different kinds of APE tag values.
# "0: Item contains text information coded in UTF-8 # "0: Item contains text information coded in UTF-8
@@ -263,7 +256,7 @@ class _CIDictProxy(DictMixin):
del(self.__dict[lower]) del(self.__dict[lower])
def keys(self): def keys(self):
return [self.__casemap.get(key, key) for key in list(self.__dict.keys())] return [self.__casemap.get(key, key) for key in self.__dict.keys()]
class APEv2(_CIDictProxy, Metadata): class APEv2(_CIDictProxy, Metadata):
@@ -280,7 +273,7 @@ class APEv2(_CIDictProxy, Metadata):
"""Return tag key=value pairs in a human-readable format.""" """Return tag key=value pairs in a human-readable format."""
items = sorted(self.items()) items = sorted(self.items())
return "\n".join("%s=%s" % (k, v.pprint()) for k, v in items) return u"\n".join(u"%s=%s" % (k, v.pprint()) for k, v in items)
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile() @loadfile()
@@ -301,7 +294,7 @@ class APEv2(_CIDictProxy, Metadata):
def __parse_tag(self, tag, count): def __parse_tag(self, tag, count):
"""Raises IOError and APEBadItemError""" """Raises IOError and APEBadItemError"""
fileobj = cBytesIO(tag) fileobj = BytesIO(tag)
for i in range(count): for i in range(count):
tag_data = fileobj.read(8) tag_data = fileobj.read(8)
@@ -330,7 +323,6 @@ class APEv2(_CIDictProxy, Metadata):
if key[-1:] == b"\x00": if key[-1:] == b"\x00":
key = key[:-1] key = key[:-1]
if PY3:
try: try:
key = key.decode("ascii") key = key.decode("ascii")
except UnicodeError as err: except UnicodeError as err:
@@ -346,16 +338,12 @@ class APEv2(_CIDictProxy, Metadata):
def __getitem__(self, key): def __getitem__(self, key):
if not is_valid_apev2_key(key): if not is_valid_apev2_key(key):
raise KeyError("%r is not a valid APEv2 key" % key) raise KeyError("%r is not a valid APEv2 key" % key)
if PY2:
key = key.encode('ascii')
return super(APEv2, self).__getitem__(key) return super(APEv2, self).__getitem__(key)
def __delitem__(self, key): def __delitem__(self, key):
if not is_valid_apev2_key(key): if not is_valid_apev2_key(key):
raise KeyError("%r is not a valid APEv2 key" % key) raise KeyError("%r is not a valid APEv2 key" % key)
if PY2:
key = key.encode('ascii')
super(APEv2, self).__delitem__(key) super(APEv2, self).__delitem__(key)
@@ -383,43 +371,28 @@ class APEv2(_CIDictProxy, Metadata):
if not is_valid_apev2_key(key): if not is_valid_apev2_key(key):
raise KeyError("%r is not a valid APEv2 key" % key) raise KeyError("%r is not a valid APEv2 key" % key)
if PY2:
key = key.encode('ascii')
if not isinstance(value, _APEValue): if not isinstance(value, _APEValue):
# let's guess at the content if we're not already a value... # let's guess at the content if we're not already a value...
if isinstance(value, text_type): if isinstance(value, str):
# unicode? we've got to be text. # unicode? we've got to be text.
value = APEValue(value, TEXT) value = APEValue(value, TEXT)
elif isinstance(value, list): elif isinstance(value, list):
items = [] items = []
for v in value: for v in value:
if not isinstance(v, text_type): if not isinstance(v, str):
if PY3:
raise TypeError("item in list not str") raise TypeError("item in list not str")
v = v.decode("utf-8")
items.append(v) items.append(v)
# list? text. # list? text.
value = APEValue("\0".join(items), TEXT) value = APEValue(u"\0".join(items), TEXT)
else: else:
if PY3:
value = APEValue(value, BINARY) value = APEValue(value, BINARY)
else:
try:
value.decode("utf-8")
except UnicodeError:
# invalid UTF8 text, probably binary
value = APEValue(value, BINARY)
else:
# valid UTF8, probably text
value = APEValue(value, TEXT)
super(APEv2, self).__setitem__(key, value) super(APEv2, self).__setitem__(key, value)
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile(writable=True, create=True) @loadfile(writable=True, create=True)
def save(self, filething): def save(self, filething=None):
"""Save changes to a file. """Save changes to a file.
If no filename is given, the one most recently loaded is used. If no filename is given, the one most recently loaded is used.
@@ -441,7 +414,7 @@ class APEv2(_CIDictProxy, Metadata):
fileobj.seek(0, 2) fileobj.seek(0, 2)
tags = [] tags = []
for key, value in list(self.items()): for key, value in self.items():
# Packed format for an item: # Packed format for an item:
# 4B: Value length # 4B: Value length
# 4B: Value type # 4B: Value type
@@ -459,7 +432,7 @@ class APEv2(_CIDictProxy, Metadata):
# "APE tags items should be sorted ascending by size... This is # "APE tags items should be sorted ascending by size... This is
# not a MUST, but STRONGLY recommended. Actually the items should # not a MUST, but STRONGLY recommended. Actually the items should
# be sorted by importance/byte, but this is not feasible." # be sorted by importance/byte, but this is not feasible."
tags.sort(key=len) tags.sort(key=lambda tag: (len(tag), tag))
num_tags = len(tags) num_tags = len(tags)
tags = b"".join(tags) tags = b"".join(tags)
@@ -481,7 +454,7 @@ class APEv2(_CIDictProxy, Metadata):
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile(writable=True) @loadfile(writable=True)
def delete(self, filething): def delete(self, filething=None):
"""Remove tags from a file.""" """Remove tags from a file."""
fileobj = filething.fileobj fileobj = filething.fileobj
@@ -578,7 +551,6 @@ class _APEValue(object):
return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind) return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind)
@swap_to_string
@total_ordering @total_ordering
class _APEUtf8Value(_APEValue): class _APEUtf8Value(_APEValue):
@@ -589,11 +561,8 @@ class _APEUtf8Value(_APEValue):
reraise(APEBadItemError, e, sys.exc_info()[2]) reraise(APEBadItemError, e, sys.exc_info()[2])
def _validate(self, value): def _validate(self, value):
if not isinstance(value, text_type): if not isinstance(value, str):
if PY3:
raise TypeError("value not str") raise TypeError("value not str")
else:
value = value.decode("utf-8")
return value return value
def _write(self): def _write(self):
@@ -627,46 +596,39 @@ class APETextValue(_APEUtf8Value, MutableSequence):
def __iter__(self): def __iter__(self):
"""Iterate over the strings of the value (not the characters)""" """Iterate over the strings of the value (not the characters)"""
return iter(self.value.split("\0")) return iter(self.value.split(u"\0"))
def __getitem__(self, index): def __getitem__(self, index):
return self.value.split("\0")[index] return self.value.split(u"\0")[index]
def __len__(self): def __len__(self):
return self.value.count("\0") + 1 return self.value.count(u"\0") + 1
def __setitem__(self, index, value): def __setitem__(self, index, value):
if not isinstance(value, text_type): if not isinstance(value, str):
if PY3:
raise TypeError("value not str") raise TypeError("value not str")
else:
value = value.decode("utf-8")
values = list(self) values = list(self)
values[index] = value values[index] = value
self.value = "\0".join(values) self.value = u"\0".join(values)
def insert(self, index, value): def insert(self, index, value):
if not isinstance(value, text_type): if not isinstance(value, str):
if PY3:
raise TypeError("value not str") raise TypeError("value not str")
else:
value = value.decode("utf-8")
values = list(self) values = list(self)
values.insert(index, value) values.insert(index, value)
self.value = "\0".join(values) self.value = u"\0".join(values)
def __delitem__(self, index): def __delitem__(self, index):
values = list(self) values = list(self)
del values[index] del values[index]
self.value = "\0".join(values) self.value = u"\0".join(values)
def pprint(self): def pprint(self):
return " / ".join(self) return u" / ".join(self)
@swap_to_string
@total_ordering @total_ordering
class APEBinaryValue(_APEValue): class APEBinaryValue(_APEValue):
"""An APEv2 binary value.""" """An APEv2 binary value."""
@@ -697,7 +659,7 @@ class APEBinaryValue(_APEValue):
return self.value < other return self.value < other
def pprint(self): def pprint(self):
return "[%d bytes]" % len(self) return u"[%d bytes]" % len(self)
class APEExtValue(_APEUtf8Value): class APEExtValue(_APEUtf8Value):
@@ -709,7 +671,7 @@ class APEExtValue(_APEUtf8Value):
kind = EXTERNAL kind = EXTERNAL
def pprint(self): def pprint(self):
return "[External] %s" % self.value return u"[External] %s" % self.value
class APEv2File(FileType): class APEv2File(FileType):
@@ -731,7 +693,7 @@ class APEv2File(FileType):
@staticmethod @staticmethod
def pprint(): def pprint():
return "Unknown format with APEv2 tag." return u"Unknown format with APEv2 tag."
@loadfile() @loadfile()
def load(self, filething): def load(self, filething):
Executable → Regular
+14 -20
View File
@@ -13,7 +13,6 @@ __all__ = ["ASF", "Open"]
from mutagen import FileType, Tags, StreamInfo from mutagen import FileType, Tags, StreamInfo
from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error
from mutagen._compat import string_types, long_, PY3, izip
from ._util import error, ASFError, ASFHeaderError from ._util import error, ASFError, ASFHeaderError
from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \ from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \
@@ -24,7 +23,7 @@ from ._attrs import ASFGUIDAttribute, ASFWordAttribute, ASFQWordAttribute, \
ASFUnicodeAttribute, ASFBaseAttribute, ASFValue ASFUnicodeAttribute, ASFBaseAttribute, ASFValue
# pyflakes # flake8
error, ASFError, ASFHeaderError, ASFValue error, ASFError, ASFHeaderError, ASFValue
@@ -51,26 +50,26 @@ class ASFInfo(StreamInfo):
sample_rate = 0 sample_rate = 0
bitrate = 0 bitrate = 0
channels = 0 channels = 0
codec_type = "" codec_type = u""
codec_name = "" codec_name = u""
codec_description = "" codec_description = u""
def __init__(self): def __init__(self):
self.length = 0.0 self.length = 0.0
self.sample_rate = 0 self.sample_rate = 0
self.bitrate = 0 self.bitrate = 0
self.channels = 0 self.channels = 0
self.codec_type = "" self.codec_type = u""
self.codec_name = "" self.codec_name = u""
self.codec_description = "" self.codec_description = u""
def pprint(self): def pprint(self):
"""Returns: """Returns:
text: a stream information text summary text: a stream information text summary
""" """
s = "ASF (%s) %d bps, %s Hz, %d channels, %.2f seconds" % ( s = u"ASF (%s) %d bps, %s Hz, %d channels, %.2f seconds" % (
self.codec_type or self.codec_name or "???", self.bitrate, self.codec_type or self.codec_name or u"???", self.bitrate,
self.sample_rate, self.channels, self.length) self.sample_rate, self.channels, self.length)
return s return s
@@ -89,7 +88,6 @@ class ASFTags(list, DictMixin, Tags):
""" """
# PY3 only
if isinstance(key, slice): if isinstance(key, slice):
return list.__getitem__(self, key) return list.__getitem__(self, key)
@@ -102,7 +100,6 @@ class ASFTags(list, DictMixin, Tags):
def __delitem__(self, key): def __delitem__(self, key):
"""Delete all values associated with the key.""" """Delete all values associated with the key."""
# PY3 only
if isinstance(key, slice): if isinstance(key, slice):
return list.__delitem__(self, key) return list.__delitem__(self, key)
@@ -129,7 +126,6 @@ class ASFTags(list, DictMixin, Tags):
string. string.
""" """
# PY3 only
if isinstance(key, slice): if isinstance(key, slice):
return list.__setitem__(self, key, values) return list.__setitem__(self, key, values)
@@ -139,16 +135,14 @@ class ASFTags(list, DictMixin, Tags):
to_append = [] to_append = []
for value in values: for value in values:
if not isinstance(value, ASFBaseAttribute): if not isinstance(value, ASFBaseAttribute):
if isinstance(value, string_types): if isinstance(value, str):
value = ASFUnicodeAttribute(value) value = ASFUnicodeAttribute(value)
elif PY3 and isinstance(value, bytes): elif isinstance(value, bytes):
value = ASFByteArrayAttribute(value) value = ASFByteArrayAttribute(value)
elif isinstance(value, bool): elif isinstance(value, bool):
value = ASFBoolAttribute(value) value = ASFBoolAttribute(value)
elif isinstance(value, int): elif isinstance(value, int):
value = ASFDWordAttribute(value) value = ASFDWordAttribute(value)
elif isinstance(value, long_):
value = ASFQWordAttribute(value)
else: else:
raise TypeError("Invalid type %r" % type(value)) raise TypeError("Invalid type %r" % type(value))
to_append.append((key, value)) to_append.append((key, value))
@@ -252,14 +246,14 @@ class ASF(FileType):
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile(writable=True) @loadfile(writable=True)
def save(self, filething, padding=None): def save(self, filething=None, padding=None):
"""save(filething=None, padding=None) """save(filething=None, padding=None)
Save tag changes back to the loaded file. Save tag changes back to the loaded file.
Args: Args:
filething (filething) filething (filething)
padding (PaddingFunction) padding (:obj:`mutagen.PaddingFunction`)
Raises: Raises:
mutagen.MutagenError mutagen.MutagenError
""" """
@@ -319,7 +313,7 @@ class ASF(FileType):
raise ASFError raise ASFError
@loadfile(writable=True) @loadfile(writable=True)
def delete(self, filething): def delete(self, filething=None):
"""delete(filething=None) """delete(filething=None)
Args: Args:
Executable → Regular
+13 -24
View File
@@ -10,8 +10,7 @@
import sys import sys
import struct import struct
from mutagen._compat import swap_to_string, text_type, PY2, reraise from mutagen._util import total_ordering, reraise
from mutagen._util import total_ordering
from ._util import ASFError from ._util import ASFError
@@ -36,7 +35,7 @@ class ASFBaseAttribute(object):
stream=None, **kwargs): stream=None, **kwargs):
self.language = language self.language = language
self.stream = stream self.stream = stream
if data: if data is not None:
self.value = self.parse(data, **kwargs) self.value = self.parse(data, **kwargs)
else: else:
if value is None: if value is None:
@@ -103,7 +102,6 @@ class ASFBaseAttribute(object):
@ASFBaseAttribute._register @ASFBaseAttribute._register
@swap_to_string
@total_ordering @total_ordering
class ASFUnicodeAttribute(ASFBaseAttribute): class ASFUnicodeAttribute(ASFBaseAttribute):
"""Unicode string attribute. """Unicode string attribute.
@@ -122,10 +120,7 @@ class ASFUnicodeAttribute(ASFBaseAttribute):
reraise(ASFError, e, sys.exc_info()[2]) reraise(ASFError, e, sys.exc_info()[2])
def _validate(self, value): def _validate(self, value):
if not isinstance(value, text_type): if not isinstance(value, str):
if PY2:
return value.decode("utf-8")
else:
raise TypeError("%r not str" % value) raise TypeError("%r not str" % value)
return value return value
@@ -142,16 +137,15 @@ class ASFUnicodeAttribute(ASFBaseAttribute):
return self.value return self.value
def __eq__(self, other): def __eq__(self, other):
return text_type(self) == other return str(self) == other
def __lt__(self, other): def __lt__(self, other):
return text_type(self) < other return str(self) < other
__hash__ = ASFBaseAttribute.__hash__ __hash__ = ASFBaseAttribute.__hash__
@ASFBaseAttribute._register @ASFBaseAttribute._register
@swap_to_string
@total_ordering @total_ordering
class ASFByteArrayAttribute(ASFBaseAttribute): class ASFByteArrayAttribute(ASFBaseAttribute):
"""Byte array attribute. """Byte array attribute.
@@ -194,7 +188,6 @@ class ASFByteArrayAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register @ASFBaseAttribute._register
@swap_to_string
@total_ordering @total_ordering
class ASFBoolAttribute(ASFBaseAttribute): class ASFBoolAttribute(ASFBaseAttribute):
"""Bool attribute. """Bool attribute.
@@ -228,10 +221,10 @@ class ASFBoolAttribute(ASFBaseAttribute):
return bool(self.value) return bool(self.value)
def __bytes__(self): def __bytes__(self):
return text_type(self.value).encode('utf-8') return str(self.value).encode('utf-8')
def __str__(self): def __str__(self):
return text_type(self.value) return str(self.value)
def __eq__(self, other): def __eq__(self, other):
return bool(self.value) == other return bool(self.value) == other
@@ -243,7 +236,6 @@ class ASFBoolAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register @ASFBaseAttribute._register
@swap_to_string
@total_ordering @total_ordering
class ASFDWordAttribute(ASFBaseAttribute): class ASFDWordAttribute(ASFBaseAttribute):
"""DWORD attribute. """DWORD attribute.
@@ -274,10 +266,10 @@ class ASFDWordAttribute(ASFBaseAttribute):
return self.value return self.value
def __bytes__(self): def __bytes__(self):
return text_type(self.value).encode('utf-8') return str(self.value).encode('utf-8')
def __str__(self): def __str__(self):
return text_type(self.value) return str(self.value)
def __eq__(self, other): def __eq__(self, other):
return int(self.value) == other return int(self.value) == other
@@ -289,7 +281,6 @@ class ASFDWordAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register @ASFBaseAttribute._register
@swap_to_string
@total_ordering @total_ordering
class ASFQWordAttribute(ASFBaseAttribute): class ASFQWordAttribute(ASFBaseAttribute):
"""QWORD attribute. """QWORD attribute.
@@ -320,10 +311,10 @@ class ASFQWordAttribute(ASFBaseAttribute):
return self.value return self.value
def __bytes__(self): def __bytes__(self):
return text_type(self.value).encode('utf-8') return str(self.value).encode('utf-8')
def __str__(self): def __str__(self):
return text_type(self.value) return str(self.value)
def __eq__(self, other): def __eq__(self, other):
return int(self.value) == other return int(self.value) == other
@@ -335,7 +326,6 @@ class ASFQWordAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register @ASFBaseAttribute._register
@swap_to_string
@total_ordering @total_ordering
class ASFWordAttribute(ASFBaseAttribute): class ASFWordAttribute(ASFBaseAttribute):
"""WORD attribute. """WORD attribute.
@@ -366,10 +356,10 @@ class ASFWordAttribute(ASFBaseAttribute):
return self.value return self.value
def __bytes__(self): def __bytes__(self):
return text_type(self.value).encode('utf-8') return str(self.value).encode('utf-8')
def __str__(self): def __str__(self):
return text_type(self.value) return str(self.value)
def __eq__(self, other): def __eq__(self, other):
return int(self.value) == other return int(self.value) == other
@@ -381,7 +371,6 @@ class ASFWordAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register @ASFBaseAttribute._register
@swap_to_string
@total_ordering @total_ordering
class ASFGUIDAttribute(ASFBaseAttribute): class ASFGUIDAttribute(ASFBaseAttribute):
"""GUID attribute.""" """GUID attribute."""
Executable → Regular
+23 -16
View File
@@ -10,7 +10,6 @@
import struct import struct
from mutagen._util import cdata, get_size from mutagen._util import cdata, get_size
from mutagen._compat import text_type, xrange, izip
from mutagen._tags import PaddingInfo from mutagen._tags import PaddingInfo
from ._util import guid2bytes, bytes2guid, CODECS, ASFError, ASFHeaderError from ._util import guid2bytes, bytes2guid, CODECS, ASFError, ASFHeaderError
@@ -108,13 +107,16 @@ class HeaderObject(BaseObject):
try: try:
data = fileobj.read(payload_size) data = fileobj.read(payload_size)
except OverflowError: except (OverflowError, MemoryError):
# read doesn't take 64bit values # read doesn't take 64bit values
raise ASFHeaderError("invalid header size") raise ASFHeaderError("invalid header size")
if len(data) != payload_size: if len(data) != payload_size:
raise ASFHeaderError("truncated") raise ASFHeaderError("truncated")
try:
obj.parse(asf, data) obj.parse(asf, data)
except struct.error:
raise ASFHeaderError("truncated")
header.objects.append(obj) header.objects.append(obj)
return header return header
@@ -151,7 +153,8 @@ class HeaderObject(BaseObject):
# ask the user for padding adjustments # ask the user for padding adjustments
file_size = get_size(fileobj) file_size = get_size(fileobj)
content_size = file_size - available content_size = file_size - available
assert content_size >= 0 if content_size < 0:
raise ASFHeaderError("truncated content")
info = PaddingInfo(available - needed_size, content_size) info = PaddingInfo(available - needed_size, content_size)
# add padding # add padding
@@ -180,11 +183,11 @@ class ContentDescriptionObject(BaseObject):
GUID = guid2bytes("75B22633-668E-11CF-A6D9-00AA0062CE6C") GUID = guid2bytes("75B22633-668E-11CF-A6D9-00AA0062CE6C")
NAMES = [ NAMES = [
"Title", u"Title",
"Author", u"Author",
"Copyright", u"Copyright",
"Description", u"Description",
"Rating", u"Rating",
] ]
def parse(self, asf, data): def parse(self, asf, data):
@@ -195,7 +198,7 @@ class ContentDescriptionObject(BaseObject):
for length in lengths: for length in lengths:
end = pos + length end = pos + length
if length > 0: if length > 0:
texts.append(data[pos:end].decode("utf-16-le").strip("\x00")) texts.append(data[pos:end].decode("utf-16-le").strip(u"\x00"))
else: else:
texts.append(None) texts.append(None)
pos = end pos = end
@@ -209,12 +212,12 @@ class ContentDescriptionObject(BaseObject):
def render_text(name): def render_text(name):
value = asf.to_content_description.get(name) value = asf.to_content_description.get(name)
if value is not None: if value is not None:
return text_type(value).encode("utf-16-le") + b"\x00\x00" return str(value).encode("utf-16-le") + b"\x00\x00"
else: else:
return b"" return b""
texts = [render_text(x) for x in self.NAMES] texts = [render_text(x) for x in self.NAMES]
data = struct.pack("<HHHHH", *list(map(len, texts))) + b"".join(texts) data = struct.pack("<HHHHH", *map(len, texts)) + b"".join(texts)
return self.GUID + struct.pack("<Q", 24 + len(data)) + data return self.GUID + struct.pack("<Q", 24 + len(data)) + data
@@ -242,7 +245,7 @@ class ExtendedContentDescriptionObject(BaseObject):
asf._tags.setdefault(self.GUID, []).append((name, attr)) asf._tags.setdefault(self.GUID, []).append((name, attr))
def render(self, asf): def render(self, asf):
attrs = list(asf.to_extended_content_description.items()) attrs = asf.to_extended_content_description.items()
data = b"".join(attr.render(name) for (name, attr) in attrs) data = b"".join(attr.render(name) for (name, attr) in attrs)
data = struct.pack("<QH", 26 + len(data), len(attrs)) + data data = struct.pack("<QH", 26 + len(data), len(attrs)) + data
return self.GUID + data return self.GUID + data
@@ -256,6 +259,8 @@ class FilePropertiesObject(BaseObject):
def parse(self, asf, data): def parse(self, asf, data):
super(FilePropertiesObject, self).parse(asf, data) super(FilePropertiesObject, self).parse(asf, data)
if len(data) < 64:
raise ASFError("invalid field property entry")
length, _, preroll = struct.unpack("<QQQ", data[40:64]) length, _, preroll = struct.unpack("<QQQ", data[40:64])
# there are files where preroll is larger than length, limit to >= 0 # there are files where preroll is larger than length, limit to >= 0
asf.info.length = max((length / 10000000.0) - (preroll / 1000.0), 0.0) asf.info.length = max((length / 10000000.0) - (preroll / 1000.0), 0.0)
@@ -292,7 +297,7 @@ class CodecListObject(BaseObject):
try: try:
name = data[offset:next_offset].decode("utf-16-le").strip("\x00") name = data[offset:next_offset].decode("utf-16-le").strip("\x00")
except UnicodeDecodeError: except UnicodeDecodeError:
name = "" name = u""
offset = next_offset offset = next_offset
units, offset = cdata.uint16_le_from(data, offset) units, offset = cdata.uint16_le_from(data, offset)
@@ -300,12 +305,12 @@ class CodecListObject(BaseObject):
try: try:
desc = data[offset:next_offset].decode("utf-16-le").strip("\x00") desc = data[offset:next_offset].decode("utf-16-le").strip("\x00")
except UnicodeDecodeError: except UnicodeDecodeError:
desc = "" desc = u""
offset = next_offset offset = next_offset
bytes_, offset = cdata.uint16_le_from(data, offset) bytes_, offset = cdata.uint16_le_from(data, offset)
next_offset = offset + bytes_ next_offset = offset + bytes_
codec = "" codec = u""
if bytes_ == 2: if bytes_ == 2:
codec_id = cdata.uint16_le_from(data, offset)[0] codec_id = cdata.uint16_le_from(data, offset)[0]
if codec_id in CODECS: if codec_id in CODECS:
@@ -377,6 +382,8 @@ class HeaderExtensionObject(BaseObject):
while datapos < datasize: while datapos < datasize:
guid, size = struct.unpack( guid, size = struct.unpack(
"<16sQ", data[22 + datapos:22 + datapos + 24]) "<16sQ", data[22 + datapos:22 + datapos + 24])
if size < 1:
raise ASFHeaderError("invalid size in header extension")
obj = BaseObject._get_object(guid) obj = BaseObject._get_object(guid)
obj.parse(asf, data[22 + datapos + 24:22 + datapos + size]) obj.parse(asf, data[22 + datapos + 24:22 + datapos + size])
self.objects.append(obj) self.objects.append(obj)
@@ -423,7 +430,7 @@ class MetadataObject(BaseObject):
asf._tags.setdefault(self.GUID, []).append((name, attr)) asf._tags.setdefault(self.GUID, []).append((name, attr))
def render(self, asf): def render(self, asf):
attrs = list(asf.to_metadata.items()) attrs = asf.to_metadata.items()
data = b"".join([attr.render_m(name) for (name, attr) in attrs]) data = b"".join([attr.render_m(name) for (name, attr) in attrs])
return (self.GUID + struct.pack("<QH", 26 + len(data), len(attrs)) + return (self.GUID + struct.pack("<QH", 26 + len(data), len(attrs)) +
data) data)
Executable → Regular
+261 -261
View File
@@ -52,265 +52,265 @@ def bytes2guid(s):
# Names from http://windows.microsoft.com/en-za/windows7/c00d10d1-[0-9A-F]{1,4} # Names from http://windows.microsoft.com/en-za/windows7/c00d10d1-[0-9A-F]{1,4}
CODECS = { CODECS = {
0x0000: "Unknown Wave Format", 0x0000: u"Unknown Wave Format",
0x0001: "Microsoft PCM Format", 0x0001: u"Microsoft PCM Format",
0x0002: "Microsoft ADPCM Format", 0x0002: u"Microsoft ADPCM Format",
0x0003: "IEEE Float", 0x0003: u"IEEE Float",
0x0004: "Compaq Computer VSELP", 0x0004: u"Compaq Computer VSELP",
0x0005: "IBM CVSD", 0x0005: u"IBM CVSD",
0x0006: "Microsoft CCITT A-Law", 0x0006: u"Microsoft CCITT A-Law",
0x0007: "Microsoft CCITT u-Law", 0x0007: u"Microsoft CCITT u-Law",
0x0008: "Microsoft DTS", 0x0008: u"Microsoft DTS",
0x0009: "Microsoft DRM", 0x0009: u"Microsoft DRM",
0x000A: "Windows Media Audio 9 Voice", 0x000A: u"Windows Media Audio 9 Voice",
0x000B: "Windows Media Audio 10 Voice", 0x000B: u"Windows Media Audio 10 Voice",
0x000C: "OGG Vorbis", 0x000C: u"OGG Vorbis",
0x000D: "FLAC", 0x000D: u"FLAC",
0x000E: "MOT AMR", 0x000E: u"MOT AMR",
0x000F: "Nice Systems IMBE", 0x000F: u"Nice Systems IMBE",
0x0010: "OKI ADPCM", 0x0010: u"OKI ADPCM",
0x0011: "Intel IMA ADPCM", 0x0011: u"Intel IMA ADPCM",
0x0012: "Videologic MediaSpace ADPCM", 0x0012: u"Videologic MediaSpace ADPCM",
0x0013: "Sierra Semiconductor ADPCM", 0x0013: u"Sierra Semiconductor ADPCM",
0x0014: "Antex Electronics G.723 ADPCM", 0x0014: u"Antex Electronics G.723 ADPCM",
0x0015: "DSP Solutions DIGISTD", 0x0015: u"DSP Solutions DIGISTD",
0x0016: "DSP Solutions DIGIFIX", 0x0016: u"DSP Solutions DIGIFIX",
0x0017: "Dialogic OKI ADPCM", 0x0017: u"Dialogic OKI ADPCM",
0x0018: "MediaVision ADPCM", 0x0018: u"MediaVision ADPCM",
0x0019: "Hewlett-Packard CU codec", 0x0019: u"Hewlett-Packard CU codec",
0x001A: "Hewlett-Packard Dynamic Voice", 0x001A: u"Hewlett-Packard Dynamic Voice",
0x0020: "Yamaha ADPCM", 0x0020: u"Yamaha ADPCM",
0x0021: "Speech Compression SONARC", 0x0021: u"Speech Compression SONARC",
0x0022: "DSP Group True Speech", 0x0022: u"DSP Group True Speech",
0x0023: "Echo Speech EchoSC1", 0x0023: u"Echo Speech EchoSC1",
0x0024: "Ahead Inc. Audiofile AF36", 0x0024: u"Ahead Inc. Audiofile AF36",
0x0025: "Audio Processing Technology APTX", 0x0025: u"Audio Processing Technology APTX",
0x0026: "Ahead Inc. AudioFile AF10", 0x0026: u"Ahead Inc. AudioFile AF10",
0x0027: "Aculab Prosody 1612", 0x0027: u"Aculab Prosody 1612",
0x0028: "Merging Technologies S.A. LRC", 0x0028: u"Merging Technologies S.A. LRC",
0x0030: "Dolby Labs AC2", 0x0030: u"Dolby Labs AC2",
0x0031: "Microsoft GSM 6.10", 0x0031: u"Microsoft GSM 6.10",
0x0032: "Microsoft MSNAudio", 0x0032: u"Microsoft MSNAudio",
0x0033: "Antex Electronics ADPCME", 0x0033: u"Antex Electronics ADPCME",
0x0034: "Control Resources VQLPC", 0x0034: u"Control Resources VQLPC",
0x0035: "DSP Solutions Digireal", 0x0035: u"DSP Solutions Digireal",
0x0036: "DSP Solutions DigiADPCM", 0x0036: u"DSP Solutions DigiADPCM",
0x0037: "Control Resources CR10", 0x0037: u"Control Resources CR10",
0x0038: "Natural MicroSystems VBXADPCM", 0x0038: u"Natural MicroSystems VBXADPCM",
0x0039: "Crystal Semiconductor IMA ADPCM", 0x0039: u"Crystal Semiconductor IMA ADPCM",
0x003A: "Echo Speech EchoSC3", 0x003A: u"Echo Speech EchoSC3",
0x003B: "Rockwell ADPCM", 0x003B: u"Rockwell ADPCM",
0x003C: "Rockwell DigiTalk", 0x003C: u"Rockwell DigiTalk",
0x003D: "Xebec Multimedia Solutions", 0x003D: u"Xebec Multimedia Solutions",
0x0040: "Antex Electronics G.721 ADPCM", 0x0040: u"Antex Electronics G.721 ADPCM",
0x0041: "Antex Electronics G.728 CELP", 0x0041: u"Antex Electronics G.728 CELP",
0x0042: "Intel G.723", 0x0042: u"Intel G.723",
0x0043: "Intel G.723.1", 0x0043: u"Intel G.723.1",
0x0044: "Intel G.729 Audio", 0x0044: u"Intel G.729 Audio",
0x0045: "Sharp G.726 Audio", 0x0045: u"Sharp G.726 Audio",
0x0050: "Microsoft MPEG-1", 0x0050: u"Microsoft MPEG-1",
0x0052: "InSoft RT24", 0x0052: u"InSoft RT24",
0x0053: "InSoft PAC", 0x0053: u"InSoft PAC",
0x0055: "MP3 - MPEG Layer III", 0x0055: u"MP3 - MPEG Layer III",
0x0059: "Lucent G.723", 0x0059: u"Lucent G.723",
0x0060: "Cirrus Logic", 0x0060: u"Cirrus Logic",
0x0061: "ESS Technology ESPCM", 0x0061: u"ESS Technology ESPCM",
0x0062: "Voxware File-Mode", 0x0062: u"Voxware File-Mode",
0x0063: "Canopus Atrac", 0x0063: u"Canopus Atrac",
0x0064: "APICOM G.726 ADPCM", 0x0064: u"APICOM G.726 ADPCM",
0x0065: "APICOM G.722 ADPCM", 0x0065: u"APICOM G.722 ADPCM",
0x0066: "Microsoft DSAT", 0x0066: u"Microsoft DSAT",
0x0067: "Microsoft DSAT Display", 0x0067: u"Microsoft DSAT Display",
0x0069: "Voxware Byte Aligned", 0x0069: u"Voxware Byte Aligned",
0x0070: "Voxware AC8", 0x0070: u"Voxware AC8",
0x0071: "Voxware AC10", 0x0071: u"Voxware AC10",
0x0072: "Voxware AC16", 0x0072: u"Voxware AC16",
0x0073: "Voxware AC20", 0x0073: u"Voxware AC20",
0x0074: "Voxware RT24 MetaVoice", 0x0074: u"Voxware RT24 MetaVoice",
0x0075: "Voxware RT29 MetaSound", 0x0075: u"Voxware RT29 MetaSound",
0x0076: "Voxware RT29HW", 0x0076: u"Voxware RT29HW",
0x0077: "Voxware VR12", 0x0077: u"Voxware VR12",
0x0078: "Voxware VR18", 0x0078: u"Voxware VR18",
0x0079: "Voxware TQ40", 0x0079: u"Voxware TQ40",
0x007A: "Voxware SC3", 0x007A: u"Voxware SC3",
0x007B: "Voxware SC3", 0x007B: u"Voxware SC3",
0x0080: "Softsound", 0x0080: u"Softsound",
0x0081: "Voxware TQ60", 0x0081: u"Voxware TQ60",
0x0082: "Microsoft MSRT24", 0x0082: u"Microsoft MSRT24",
0x0083: "AT&T Labs G.729A", 0x0083: u"AT&T Labs G.729A",
0x0084: "Motion Pixels MVI MV12", 0x0084: u"Motion Pixels MVI MV12",
0x0085: "DataFusion Systems G.726", 0x0085: u"DataFusion Systems G.726",
0x0086: "DataFusion Systems GSM610", 0x0086: u"DataFusion Systems GSM610",
0x0088: "Iterated Systems ISIAudio", 0x0088: u"Iterated Systems ISIAudio",
0x0089: "Onlive", 0x0089: u"Onlive",
0x008A: "Multitude FT SX20", 0x008A: u"Multitude FT SX20",
0x008B: "Infocom ITS ACM G.721", 0x008B: u"Infocom ITS ACM G.721",
0x008C: "Convedia G.729", 0x008C: u"Convedia G.729",
0x008D: "Congruency Audio", 0x008D: u"Congruency Audio",
0x0091: "Siemens Business Communications SBC24", 0x0091: u"Siemens Business Communications SBC24",
0x0092: "Sonic Foundry Dolby AC3 SPDIF", 0x0092: u"Sonic Foundry Dolby AC3 SPDIF",
0x0093: "MediaSonic G.723", 0x0093: u"MediaSonic G.723",
0x0094: "Aculab Prosody 8KBPS", 0x0094: u"Aculab Prosody 8KBPS",
0x0097: "ZyXEL ADPCM", 0x0097: u"ZyXEL ADPCM",
0x0098: "Philips LPCBB", 0x0098: u"Philips LPCBB",
0x0099: "Studer Professional Audio AG Packed", 0x0099: u"Studer Professional Audio AG Packed",
0x00A0: "Malden Electronics PHONYTALK", 0x00A0: u"Malden Electronics PHONYTALK",
0x00A1: "Racal Recorder GSM", 0x00A1: u"Racal Recorder GSM",
0x00A2: "Racal Recorder G720.a", 0x00A2: u"Racal Recorder G720.a",
0x00A3: "Racal Recorder G723.1", 0x00A3: u"Racal Recorder G723.1",
0x00A4: "Racal Recorder Tetra ACELP", 0x00A4: u"Racal Recorder Tetra ACELP",
0x00B0: "NEC AAC", 0x00B0: u"NEC AAC",
0x00FF: "CoreAAC Audio", 0x00FF: u"CoreAAC Audio",
0x0100: "Rhetorex ADPCM", 0x0100: u"Rhetorex ADPCM",
0x0101: "BeCubed Software IRAT", 0x0101: u"BeCubed Software IRAT",
0x0111: "Vivo G.723", 0x0111: u"Vivo G.723",
0x0112: "Vivo Siren", 0x0112: u"Vivo Siren",
0x0120: "Philips CELP", 0x0120: u"Philips CELP",
0x0121: "Philips Grundig", 0x0121: u"Philips Grundig",
0x0123: "Digital G.723", 0x0123: u"Digital G.723",
0x0125: "Sanyo ADPCM", 0x0125: u"Sanyo ADPCM",
0x0130: "Sipro Lab Telecom ACELP.net", 0x0130: u"Sipro Lab Telecom ACELP.net",
0x0131: "Sipro Lab Telecom ACELP.4800", 0x0131: u"Sipro Lab Telecom ACELP.4800",
0x0132: "Sipro Lab Telecom ACELP.8V3", 0x0132: u"Sipro Lab Telecom ACELP.8V3",
0x0133: "Sipro Lab Telecom ACELP.G.729", 0x0133: u"Sipro Lab Telecom ACELP.G.729",
0x0134: "Sipro Lab Telecom ACELP.G.729A", 0x0134: u"Sipro Lab Telecom ACELP.G.729A",
0x0135: "Sipro Lab Telecom ACELP.KELVIN", 0x0135: u"Sipro Lab Telecom ACELP.KELVIN",
0x0136: "VoiceAge AMR", 0x0136: u"VoiceAge AMR",
0x0140: "Dictaphone G.726 ADPCM", 0x0140: u"Dictaphone G.726 ADPCM",
0x0141: "Dictaphone CELP68", 0x0141: u"Dictaphone CELP68",
0x0142: "Dictaphone CELP54", 0x0142: u"Dictaphone CELP54",
0x0150: "Qualcomm PUREVOICE", 0x0150: u"Qualcomm PUREVOICE",
0x0151: "Qualcomm HALFRATE", 0x0151: u"Qualcomm HALFRATE",
0x0155: "Ring Zero Systems TUBGSM", 0x0155: u"Ring Zero Systems TUBGSM",
0x0160: "Windows Media Audio Standard", 0x0160: u"Windows Media Audio Standard",
0x0161: "Windows Media Audio 9 Standard", 0x0161: u"Windows Media Audio 9 Standard",
0x0162: "Windows Media Audio 9 Professional", 0x0162: u"Windows Media Audio 9 Professional",
0x0163: "Windows Media Audio 9 Lossless", 0x0163: u"Windows Media Audio 9 Lossless",
0x0164: "Windows Media Audio Pro over SPDIF", 0x0164: u"Windows Media Audio Pro over SPDIF",
0x0170: "Unisys NAP ADPCM", 0x0170: u"Unisys NAP ADPCM",
0x0171: "Unisys NAP ULAW", 0x0171: u"Unisys NAP ULAW",
0x0172: "Unisys NAP ALAW", 0x0172: u"Unisys NAP ALAW",
0x0173: "Unisys NAP 16K", 0x0173: u"Unisys NAP 16K",
0x0174: "Sycom ACM SYC008", 0x0174: u"Sycom ACM SYC008",
0x0175: "Sycom ACM SYC701 G725", 0x0175: u"Sycom ACM SYC701 G725",
0x0176: "Sycom ACM SYC701 CELP54", 0x0176: u"Sycom ACM SYC701 CELP54",
0x0177: "Sycom ACM SYC701 CELP68", 0x0177: u"Sycom ACM SYC701 CELP68",
0x0178: "Knowledge Adventure ADPCM", 0x0178: u"Knowledge Adventure ADPCM",
0x0180: "Fraunhofer IIS MPEG-2 AAC", 0x0180: u"Fraunhofer IIS MPEG-2 AAC",
0x0190: "Digital Theater Systems DTS", 0x0190: u"Digital Theater Systems DTS",
0x0200: "Creative Labs ADPCM", 0x0200: u"Creative Labs ADPCM",
0x0202: "Creative Labs FastSpeech8", 0x0202: u"Creative Labs FastSpeech8",
0x0203: "Creative Labs FastSpeech10", 0x0203: u"Creative Labs FastSpeech10",
0x0210: "UHER informatic GmbH ADPCM", 0x0210: u"UHER informatic GmbH ADPCM",
0x0215: "Ulead DV Audio", 0x0215: u"Ulead DV Audio",
0x0216: "Ulead DV Audio", 0x0216: u"Ulead DV Audio",
0x0220: "Quarterdeck", 0x0220: u"Quarterdeck",
0x0230: "I-link Worldwide ILINK VC", 0x0230: u"I-link Worldwide ILINK VC",
0x0240: "Aureal Semiconductor RAW SPORT", 0x0240: u"Aureal Semiconductor RAW SPORT",
0x0249: "Generic Passthru", 0x0249: u"Generic Passthru",
0x0250: "Interactive Products HSX", 0x0250: u"Interactive Products HSX",
0x0251: "Interactive Products RPELP", 0x0251: u"Interactive Products RPELP",
0x0260: "Consistent Software CS2", 0x0260: u"Consistent Software CS2",
0x0270: "Sony SCX", 0x0270: u"Sony SCX",
0x0271: "Sony SCY", 0x0271: u"Sony SCY",
0x0272: "Sony ATRAC3", 0x0272: u"Sony ATRAC3",
0x0273: "Sony SPC", 0x0273: u"Sony SPC",
0x0280: "Telum Audio", 0x0280: u"Telum Audio",
0x0281: "Telum IA Audio", 0x0281: u"Telum IA Audio",
0x0285: "Norcom Voice Systems ADPCM", 0x0285: u"Norcom Voice Systems ADPCM",
0x0300: "Fujitsu TOWNS SND", 0x0300: u"Fujitsu TOWNS SND",
0x0350: "Micronas SC4 Speech", 0x0350: u"Micronas SC4 Speech",
0x0351: "Micronas CELP833", 0x0351: u"Micronas CELP833",
0x0400: "Brooktree BTV Digital", 0x0400: u"Brooktree BTV Digital",
0x0401: "Intel Music Coder", 0x0401: u"Intel Music Coder",
0x0402: "Intel Audio", 0x0402: u"Intel Audio",
0x0450: "QDesign Music", 0x0450: u"QDesign Music",
0x0500: "On2 AVC0 Audio", 0x0500: u"On2 AVC0 Audio",
0x0501: "On2 AVC1 Audio", 0x0501: u"On2 AVC1 Audio",
0x0680: "AT&T Labs VME VMPCM", 0x0680: u"AT&T Labs VME VMPCM",
0x0681: "AT&T Labs TPC", 0x0681: u"AT&T Labs TPC",
0x08AE: "ClearJump Lightwave Lossless", 0x08AE: u"ClearJump Lightwave Lossless",
0x1000: "Olivetti GSM", 0x1000: u"Olivetti GSM",
0x1001: "Olivetti ADPCM", 0x1001: u"Olivetti ADPCM",
0x1002: "Olivetti CELP", 0x1002: u"Olivetti CELP",
0x1003: "Olivetti SBC", 0x1003: u"Olivetti SBC",
0x1004: "Olivetti OPR", 0x1004: u"Olivetti OPR",
0x1100: "Lernout & Hauspie", 0x1100: u"Lernout & Hauspie",
0x1101: "Lernout & Hauspie CELP", 0x1101: u"Lernout & Hauspie CELP",
0x1102: "Lernout & Hauspie SBC8", 0x1102: u"Lernout & Hauspie SBC8",
0x1103: "Lernout & Hauspie SBC12", 0x1103: u"Lernout & Hauspie SBC12",
0x1104: "Lernout & Hauspie SBC16", 0x1104: u"Lernout & Hauspie SBC16",
0x1400: "Norris Communication", 0x1400: u"Norris Communication",
0x1401: "ISIAudio", 0x1401: u"ISIAudio",
0x1500: "AT&T Labs Soundspace Music Compression", 0x1500: u"AT&T Labs Soundspace Music Compression",
0x1600: "Microsoft MPEG ADTS AAC", 0x1600: u"Microsoft MPEG ADTS AAC",
0x1601: "Microsoft MPEG RAW AAC", 0x1601: u"Microsoft MPEG RAW AAC",
0x1608: "Nokia MPEG ADTS AAC", 0x1608: u"Nokia MPEG ADTS AAC",
0x1609: "Nokia MPEG RAW AAC", 0x1609: u"Nokia MPEG RAW AAC",
0x181C: "VoxWare MetaVoice RT24", 0x181C: u"VoxWare MetaVoice RT24",
0x1971: "Sonic Foundry Lossless", 0x1971: u"Sonic Foundry Lossless",
0x1979: "Innings Telecom ADPCM", 0x1979: u"Innings Telecom ADPCM",
0x1FC4: "NTCSoft ALF2CD ACM", 0x1FC4: u"NTCSoft ALF2CD ACM",
0x2000: "Dolby AC3", 0x2000: u"Dolby AC3",
0x2001: "DTS", 0x2001: u"DTS",
0x4143: "Divio AAC", 0x4143: u"Divio AAC",
0x4201: "Nokia Adaptive Multi-Rate", 0x4201: u"Nokia Adaptive Multi-Rate",
0x4243: "Divio G.726", 0x4243: u"Divio G.726",
0x4261: "ITU-T H.261", 0x4261: u"ITU-T H.261",
0x4263: "ITU-T H.263", 0x4263: u"ITU-T H.263",
0x4264: "ITU-T H.264", 0x4264: u"ITU-T H.264",
0x674F: "Ogg Vorbis Mode 1", 0x674F: u"Ogg Vorbis Mode 1",
0x6750: "Ogg Vorbis Mode 2", 0x6750: u"Ogg Vorbis Mode 2",
0x6751: "Ogg Vorbis Mode 3", 0x6751: u"Ogg Vorbis Mode 3",
0x676F: "Ogg Vorbis Mode 1+", 0x676F: u"Ogg Vorbis Mode 1+",
0x6770: "Ogg Vorbis Mode 2+", 0x6770: u"Ogg Vorbis Mode 2+",
0x6771: "Ogg Vorbis Mode 3+", 0x6771: u"Ogg Vorbis Mode 3+",
0x7000: "3COM NBX Audio", 0x7000: u"3COM NBX Audio",
0x706D: "FAAD AAC Audio", 0x706D: u"FAAD AAC Audio",
0x77A1: "True Audio Lossless Audio", 0x77A1: u"True Audio Lossless Audio",
0x7A21: "GSM-AMR CBR 3GPP Audio", 0x7A21: u"GSM-AMR CBR 3GPP Audio",
0x7A22: "GSM-AMR VBR 3GPP Audio", 0x7A22: u"GSM-AMR VBR 3GPP Audio",
0xA100: "Comverse Infosys G723.1", 0xA100: u"Comverse Infosys G723.1",
0xA101: "Comverse Infosys AVQSBC", 0xA101: u"Comverse Infosys AVQSBC",
0xA102: "Comverse Infosys SBC", 0xA102: u"Comverse Infosys SBC",
0xA103: "Symbol Technologies G729a", 0xA103: u"Symbol Technologies G729a",
0xA104: "VoiceAge AMR WB", 0xA104: u"VoiceAge AMR WB",
0xA105: "Ingenient Technologies G.726", 0xA105: u"Ingenient Technologies G.726",
0xA106: "ISO/MPEG-4 Advanced Audio Coding (AAC)", 0xA106: u"ISO/MPEG-4 Advanced Audio Coding (AAC)",
0xA107: "Encore Software Ltd's G.726", 0xA107: u"Encore Software Ltd's G.726",
0xA108: "ZOLL Medical Corporation ASAO", 0xA108: u"ZOLL Medical Corporation ASAO",
0xA109: "Speex Voice", 0xA109: u"Speex Voice",
0xA10A: "Vianix MASC Speech Compression", 0xA10A: u"Vianix MASC Speech Compression",
0xA10B: "Windows Media 9 Spectrum Analyzer Output", 0xA10B: u"Windows Media 9 Spectrum Analyzer Output",
0xA10C: "Media Foundation Spectrum Analyzer Output", 0xA10C: u"Media Foundation Spectrum Analyzer Output",
0xA10D: "GSM 6.10 (Full-Rate) Speech", 0xA10D: u"GSM 6.10 (Full-Rate) Speech",
0xA10E: "GSM 6.20 (Half-Rate) Speech", 0xA10E: u"GSM 6.20 (Half-Rate) Speech",
0xA10F: "GSM 6.60 (Enchanced Full-Rate) Speech", 0xA10F: u"GSM 6.60 (Enchanced Full-Rate) Speech",
0xA110: "GSM 6.90 (Adaptive Multi-Rate) Speech", 0xA110: u"GSM 6.90 (Adaptive Multi-Rate) Speech",
0xA111: "GSM Adaptive Multi-Rate WideBand Speech", 0xA111: u"GSM Adaptive Multi-Rate WideBand Speech",
0xA112: "Polycom G.722", 0xA112: u"Polycom G.722",
0xA113: "Polycom G.728", 0xA113: u"Polycom G.728",
0xA114: "Polycom G.729a", 0xA114: u"Polycom G.729a",
0xA115: "Polycom Siren", 0xA115: u"Polycom Siren",
0xA116: "Global IP Sound ILBC", 0xA116: u"Global IP Sound ILBC",
0xA117: "Radio Time Time Shifted Radio", 0xA117: u"Radio Time Time Shifted Radio",
0xA118: "Nice Systems ACA", 0xA118: u"Nice Systems ACA",
0xA119: "Nice Systems ADPCM", 0xA119: u"Nice Systems ADPCM",
0xA11A: "Vocord Group ITU-T G.721", 0xA11A: u"Vocord Group ITU-T G.721",
0xA11B: "Vocord Group ITU-T G.726", 0xA11B: u"Vocord Group ITU-T G.726",
0xA11C: "Vocord Group ITU-T G.722.1", 0xA11C: u"Vocord Group ITU-T G.722.1",
0xA11D: "Vocord Group ITU-T G.728", 0xA11D: u"Vocord Group ITU-T G.728",
0xA11E: "Vocord Group ITU-T G.729", 0xA11E: u"Vocord Group ITU-T G.729",
0xA11F: "Vocord Group ITU-T G.729a", 0xA11F: u"Vocord Group ITU-T G.729a",
0xA120: "Vocord Group ITU-T G.723.1", 0xA120: u"Vocord Group ITU-T G.723.1",
0xA121: "Vocord Group LBC", 0xA121: u"Vocord Group LBC",
0xA122: "Nice G.728", 0xA122: u"Nice G.728",
0xA123: "France Telecom G.729 ACM Audio", 0xA123: u"France Telecom G.729 ACM Audio",
0xA124: "CODIAN Audio", 0xA124: u"CODIAN Audio",
0xCC12: "Intel YUV12 Codec", 0xCC12: u"Intel YUV12 Codec",
0xCFCC: "Digital Processing Systems Perception Motion JPEG", 0xCFCC: u"Digital Processing Systems Perception Motion JPEG",
0xD261: "DEC H.261", 0xD261: u"DEC H.261",
0xD263: "DEC H.263", 0xD263: u"DEC H.263",
0xFFFE: "Extensible Wave Format", 0xFFFE: u"Extensible Wave Format",
0xFFFF: "Unregistered", 0xFFFF: u"Unregistered",
} }
+267
View File
@@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2020 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""DSDIFF audio stream information and tags."""
import struct
from mutagen import StreamInfo
from mutagen._file import FileType
from mutagen._iff import (
IffChunk,
IffContainerChunkMixin,
IffID3,
IffFile,
InvalidChunk,
error as IffError,
)
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import (
convert_error,
loadfile,
endswith,
)
__all__ = ["DSDIFF", "Open", "delete"]
class error(IffError):
pass
# See
# https://dsd-guide.com/sites/default/files/white-papers/DSDIFF_1.5_Spec.pdf
class DSDIFFChunk(IffChunk):
"""Representation of a single DSDIFF chunk"""
HEADER_SIZE = 12
@classmethod
def parse_header(cls, header):
return struct.unpack('>4sQ', header)
@classmethod
def get_class(cls, id):
if id in DSDIFFListChunk.LIST_CHUNK_IDS:
return DSDIFFListChunk
elif id == 'DST':
return DSTChunk
else:
return cls
def write_new_header(self, id_, size):
self._fileobj.write(struct.pack('>4sQ', id_, size))
def write_size(self):
self._fileobj.write(struct.pack('>Q', self.data_size))
class DSDIFFListChunk(DSDIFFChunk, IffContainerChunkMixin):
"""A DSDIFF chunk containing other chunks.
"""
LIST_CHUNK_IDS = ['FRM8', 'PROP']
def parse_next_subchunk(self):
return DSDIFFChunk.parse(self._fileobj, self)
def __init__(self, fileobj, id, data_size, parent_chunk):
if id not in self.LIST_CHUNK_IDS:
raise InvalidChunk('Not a list chunk: %s' % id)
DSDIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk)
self.init_container()
class DSTChunk(DSDIFFChunk, IffContainerChunkMixin):
"""A DSDIFF chunk containing other chunks.
"""
def parse_next_subchunk(self):
return DSDIFFChunk.parse(self._fileobj, self)
def __init__(self, fileobj, id, data_size, parent_chunk):
if id != 'DST':
raise InvalidChunk('Not a DST chunk: %s' % id)
DSDIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk)
self.init_container(name_size=0)
class DSDIFFFile(IffFile):
"""Representation of a DSDIFF file"""
def __init__(self, fileobj):
super().__init__(DSDIFFChunk, fileobj)
if self.root.id != u'FRM8':
raise InvalidChunk("Root chunk must be a FRM8 chunk, got %r"
% self.root)
class DSDIFFInfo(StreamInfo):
"""DSDIFF stream information.
Attributes:
channels (`int`): number of audio channels
length (`float`): file length in seconds, as a float
sample_rate (`int`): audio sampling rate in Hz
bits_per_sample (`int`): audio sample size (for DSD this is always 1)
bitrate (`int`): audio bitrate, in bits per second
compression (`str`): DSD (uncompressed) or DST
"""
channels = 0
length = 0
sample_rate = 0
bits_per_sample = 1
bitrate = 0
compression = None
@convert_error(IOError, error)
def __init__(self, fileobj):
"""Raises error"""
iff = DSDIFFFile(fileobj)
try:
prop_chunk = iff['PROP']
except KeyError as e:
raise error(str(e))
if prop_chunk.name == 'SND ':
for chunk in prop_chunk.subchunks():
if chunk.id == 'FS' and chunk.data_size == 4:
data = chunk.read()
if len(data) < 4:
raise InvalidChunk("Not enough data in FS chunk")
self.sample_rate, = struct.unpack('>L', data[:4])
elif chunk.id == 'CHNL' and chunk.data_size >= 2:
data = chunk.read()
if len(data) < 2:
raise InvalidChunk("Not enough data in CHNL chunk")
self.channels, = struct.unpack('>H', data[:2])
elif chunk.id == 'CMPR' and chunk.data_size >= 4:
data = chunk.read()
if len(data) < 4:
raise InvalidChunk("Not enough data in CMPR chunk")
compression_id, = struct.unpack('>4s', data[:4])
self.compression = compression_id.decode('ascii').rstrip()
if self.sample_rate < 0:
raise error("Invalid sample rate")
if self.compression == 'DSD': # not compressed
try:
dsd_chunk = iff['DSD']
except KeyError as e:
raise error(str(e))
# DSD data has one bit per sample. Eight samples of a channel
# are clustered together for a channel byte. For multiple channels
# the channel bytes are interleaved (in the order specified in the
# CHNL chunk). See DSDIFF spec chapter 3.3.
sample_count = dsd_chunk.data_size * 8 / (self.channels or 1)
if self.sample_rate != 0:
self.length = sample_count / float(self.sample_rate)
self.bitrate = (self.channels * self.bits_per_sample
* self.sample_rate)
elif self.compression == 'DST':
try:
dst_frame = iff['DST']
dst_frame_info = dst_frame['FRTE']
except KeyError as e:
raise error(str(e))
if dst_frame_info.data_size >= 6:
data = dst_frame_info.read()
if len(data) < 6:
raise InvalidChunk("Not enough data in FRTE chunk")
frame_count, frame_rate = struct.unpack('>LH', data[:6])
if frame_rate:
self.length = frame_count / frame_rate
if frame_count:
dst_data_size = dst_frame.data_size - dst_frame_info.size
avg_frame_size = dst_data_size / frame_count
self.bitrate = avg_frame_size * 8 * frame_rate
def pprint(self):
return u"%d channel DSDIFF (%s) @ %d bps, %s Hz, %.2f seconds" % (
self.channels, self.compression, self.bitrate, self.sample_rate,
self.length)
class _DSDIFFID3(IffID3):
"""A DSDIFF file with ID3v2 tags"""
def _load_file(self, fileobj):
return DSDIFFFile(fileobj)
@convert_error(IOError, error)
@loadfile(method=False, writable=True)
def delete(filething):
"""Completely removes the ID3 chunk from the DSDIFF file"""
try:
del DSDIFFFile(filething.fileobj)[u'ID3']
except KeyError:
pass
class DSDIFF(FileType):
"""DSDIFF(filething)
An DSDIFF audio file.
For tagging ID3v2 data is added to a chunk with the ID "ID3 ".
Arguments:
filething (filething)
Attributes:
tags (`mutagen.id3.ID3`)
info (`DSDIFFInfo`)
"""
_mimes = ["audio/x-dff"]
@convert_error(IOError, error)
@loadfile()
def load(self, filething, **kwargs):
fileobj = filething.fileobj
try:
self.tags = _DSDIFFID3(fileobj, **kwargs)
except ID3NoHeaderError:
self.tags = None
except ID3Error as e:
raise error(e)
else:
self.tags.filename = self.filename
fileobj.seek(0, 0)
self.info = DSDIFFInfo(fileobj)
def add_tags(self):
"""Add empty ID3 tags to the file."""
if self.tags is None:
self.tags = _DSDIFFID3()
else:
raise error("an ID3 tag already exists")
@staticmethod
def score(filename, fileobj, header):
return header.startswith(b"FRM8") * 2 + endswith(filename, ".dff")
Open = DSDIFF
Executable → Regular
+12 -12
View File
@@ -11,11 +11,11 @@
import sys import sys
import struct import struct
from io import BytesIO
from ._compat import cBytesIO, reraise, endswith
from mutagen import FileType, StreamInfo from mutagen import FileType, StreamInfo
from mutagen._util import cdata, MutagenError, loadfile, convert_error from mutagen._util import cdata, MutagenError, loadfile, \
convert_error, reraise, endswith
from mutagen.id3 import ID3 from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
@@ -80,7 +80,7 @@ class DSDChunk(DSFChunk):
self.offset_metdata_chunk = cdata.ulonglong_le(data[20:28]) self.offset_metdata_chunk = cdata.ulonglong_le(data[20:28])
def write(self): def write(self):
f = cBytesIO() f = BytesIO()
f.write(self.chunk_header) f.write(self.chunk_header)
f.write(struct.pack("<Q", DSDChunk.CHUNK_SIZE)) f.write(struct.pack("<Q", DSDChunk.CHUNK_SIZE))
f.write(struct.pack("<Q", self.total_size)) f.write(struct.pack("<Q", self.total_size))
@@ -90,8 +90,8 @@ class DSDChunk(DSFChunk):
self.fileobj.write(f.getvalue()) self.fileobj.write(f.getvalue())
def pprint(self): def pprint(self):
return ("DSD Chunk (Total file size = %d, " return (u"DSD Chunk (Total file size = %d, "
"Pointer to Metadata chunk = %d)" % ( u"Pointer to Metadata chunk = %d)" % (
self.total_size, self.offset_metdata_chunk)) self.total_size, self.offset_metdata_chunk))
@@ -148,8 +148,8 @@ class FormatChunk(DSFChunk):
self.sample_count = cdata.ulonglong_le(data[36:44]) self.sample_count = cdata.ulonglong_le(data[36:44])
def pprint(self): def pprint(self):
return "fmt Chunk (Channel Type = %d, Channel Num = %d, " \ return u"fmt Chunk (Channel Type = %d, Channel Num = %d, " \
"Sampling Frequency = %d, %.2f seconds)" % \ u"Sampling Frequency = %d, %.2f seconds)" % \
(self.channel_type, self.channel_num, self.sampling_frequency, (self.channel_type, self.channel_num, self.sampling_frequency,
self.length) self.length)
@@ -181,7 +181,7 @@ class DataChunk(DSFChunk):
raise error("DSF data header size mismatch") raise error("DSF data header size mismatch")
def pprint(self): def pprint(self):
return "data Chunk (Chunk Offset = %d, Chunk Size = %d)" % ( return u"data Chunk (Chunk Offset = %d, Chunk Size = %d)" % (
self.chunk_offset, self.chunk_size) self.chunk_offset, self.chunk_size)
@@ -199,7 +199,7 @@ class _DSFID3(ID3):
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile(writable=True) @loadfile(writable=True)
def save(self, filething, v2_version=4, v23_sep='/', padding=None): def save(self, filething=None, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the DSF file""" """Save ID3v2 data to the DSF file"""
fileobj = filething.fileobj fileobj = filething.fileobj
@@ -269,7 +269,7 @@ class DSFInfo(StreamInfo):
return self.sample_rate * self.bits_per_sample * self.channels return self.sample_rate * self.bits_per_sample * self.channels
def pprint(self): def pprint(self):
return "%d channel DSF @ %d bits, %s Hz, %.2f seconds" % ( return u"%d channel DSF @ %d bits, %s Hz, %.2f seconds" % (
self.channels, self.bits_per_sample, self.sample_rate, self.length) self.channels, self.bits_per_sample, self.sample_rate, self.length)
@@ -328,7 +328,7 @@ class DSF(FileType):
self.info = DSFInfo(dsf_file.fmt_chunk) self.info = DSFInfo(dsf_file.fmt_chunk)
@loadfile(writable=True) @loadfile(writable=True)
def delete(self, filething): def delete(self, filething=None):
self.tags = None self.tags = None
delete(filething) delete(filething)
Executable → Regular
+33 -36
View File
@@ -14,7 +14,6 @@ more like Vorbis or APEv2 tags.
import mutagen.id3 import mutagen.id3
from ._compat import iteritems, text_type, PY2
from mutagen import Metadata from mutagen import Metadata
from mutagen._util import DictMixin, dict_match, loadfile from mutagen._util import DictMixin, dict_match, loadfile
from mutagen.id3 import ID3, error, delete, ID3FileType from mutagen.id3 import ID3, error, delete, ID3FileType
@@ -153,7 +152,7 @@ class EasyID3(DictMixin, Metadata):
enc = 0 enc = 0
# Store 8859-1 if we can, per MusicBrainz spec. # Store 8859-1 if we can, per MusicBrainz spec.
for v in value: for v in value:
if v and max(v) > '\x7f': if v and max(v) > u'\x7f':
enc = 3 enc = 3
break break
@@ -173,7 +172,8 @@ class EasyID3(DictMixin, Metadata):
lambda s, v: setattr(s.__id3, 'load', v)) lambda s, v: setattr(s.__id3, 'load', v))
@loadfile(writable=True, create=True) @loadfile(writable=True, create=True)
def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
padding=None):
"""save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)
Save changes to a file. Save changes to a file.
@@ -203,8 +203,9 @@ class EasyID3(DictMixin, Metadata):
filename = property(lambda s: s.__id3.filename, filename = property(lambda s: s.__id3.filename,
lambda s, fn: setattr(s.__id3, 'filename', fn)) lambda s, fn: setattr(s.__id3, 'filename', fn))
size = property(lambda s: s.__id3.size, @property
lambda s, fn: setattr(s.__id3, 'size', s)) def size(self):
return self.__id3.size
def __getitem__(self, key): def __getitem__(self, key):
func = dict_match(self.Get, key.lower(), self.GetFallback) func = dict_match(self.Get, key.lower(), self.GetFallback)
@@ -214,12 +215,8 @@ class EasyID3(DictMixin, Metadata):
raise EasyID3KeyError("%r is not a valid key" % key) raise EasyID3KeyError("%r is not a valid key" % key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
if PY2:
if isinstance(value, str): if isinstance(value, str):
value = [value] value = [value]
else:
if isinstance(value, text_type):
value = [value]
func = dict_match(self.Set, key.lower(), self.SetFallback) func = dict_match(self.Set, key.lower(), self.SetFallback)
if func is not None: if func is not None:
return func(self.__id3, key, value) return func(self.__id3, key, value)
@@ -235,7 +232,7 @@ class EasyID3(DictMixin, Metadata):
def keys(self): def keys(self):
keys = [] keys = []
for key in list(self.Get.keys()): for key in self.Get.keys():
if key in self.List: if key in self.List:
keys.extend(self.List[key](self.__id3, key)) keys.extend(self.List[key](self.__id3, key))
elif key in self: elif key in self:
@@ -398,7 +395,7 @@ def gain_get(id3, key):
except KeyError: except KeyError:
raise EasyID3KeyError(key) raise EasyID3KeyError(key)
else: else:
return ["%+f dB" % frame.gain] return [u"%+f dB" % frame.gain]
def gain_set(id3, key, value): def gain_set(id3, key, value):
@@ -432,7 +429,7 @@ def peak_get(id3, key):
except KeyError: except KeyError:
raise EasyID3KeyError(key) raise EasyID3KeyError(key)
else: else:
return ["%f" % frame.peak] return [u"%f" % frame.peak]
def peak_set(id3, key, value): def peak_set(id3, key, value):
@@ -469,7 +466,7 @@ def peakgain_list(id3, key):
keys.append("replaygain_%s_peak" % frame.desc) keys.append("replaygain_%s_peak" % frame.desc)
return keys return keys
for frameid, key in iteritems({ for frameid, key in {
"TALB": "album", "TALB": "album",
"TBPM": "bpm", "TBPM": "bpm",
"TCMP": "compilation", # iTunes extension "TCMP": "compilation", # iTunes extension
@@ -498,7 +495,7 @@ for frameid, key in iteritems({
"TSRC": "isrc", "TSRC": "isrc",
"TSST": "discsubtitle", "TSST": "discsubtitle",
"TLAN": "language", "TLAN": "language",
}): }.items():
EasyID3.RegisterTextKey(key, frameid) EasyID3.RegisterTextKey(key, frameid)
EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete) EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete)
@@ -519,28 +516,28 @@ EasyID3.RegisterKey("replaygain_*_peak", peak_get, peak_set, peak_delete)
# http://musicbrainz.org/docs/specs/metadata_tags.html # http://musicbrainz.org/docs/specs/metadata_tags.html
# http://bugs.musicbrainz.org/ticket/1383 # http://bugs.musicbrainz.org/ticket/1383
# http://musicbrainz.org/doc/MusicBrainzTag # http://musicbrainz.org/doc/MusicBrainzTag
for desc, key in iteritems({ for desc, key in {
"MusicBrainz Artist Id": "musicbrainz_artistid", u"MusicBrainz Artist Id": "musicbrainz_artistid",
"MusicBrainz Album Id": "musicbrainz_albumid", u"MusicBrainz Album Id": "musicbrainz_albumid",
"MusicBrainz Album Artist Id": "musicbrainz_albumartistid", u"MusicBrainz Album Artist Id": "musicbrainz_albumartistid",
"MusicBrainz TRM Id": "musicbrainz_trmid", u"MusicBrainz TRM Id": "musicbrainz_trmid",
"MusicIP PUID": "musicip_puid", u"MusicIP PUID": "musicip_puid",
"MusicMagic Fingerprint": "musicip_fingerprint", u"MusicMagic Fingerprint": "musicip_fingerprint",
"MusicBrainz Album Status": "musicbrainz_albumstatus", u"MusicBrainz Album Status": "musicbrainz_albumstatus",
"MusicBrainz Album Type": "musicbrainz_albumtype", u"MusicBrainz Album Type": "musicbrainz_albumtype",
"MusicBrainz Album Release Country": "releasecountry", u"MusicBrainz Album Release Country": "releasecountry",
"MusicBrainz Disc Id": "musicbrainz_discid", u"MusicBrainz Disc Id": "musicbrainz_discid",
"ASIN": "asin", u"ASIN": "asin",
"ALBUMARTISTSORT": "albumartistsort", u"ALBUMARTISTSORT": "albumartistsort",
"PERFORMER": "performer", u"PERFORMER": "performer",
"BARCODE": "barcode", u"BARCODE": "barcode",
"CATALOGNUMBER": "catalognumber", u"CATALOGNUMBER": "catalognumber",
"MusicBrainz Release Track Id": "musicbrainz_releasetrackid", u"MusicBrainz Release Track Id": "musicbrainz_releasetrackid",
"MusicBrainz Release Group Id": "musicbrainz_releasegroupid", u"MusicBrainz Release Group Id": "musicbrainz_releasegroupid",
"MusicBrainz Work Id": "musicbrainz_workid", u"MusicBrainz Work Id": "musicbrainz_workid",
"Acoustid Fingerprint": "acoustid_fingerprint", u"Acoustid Fingerprint": "acoustid_fingerprint",
"Acoustid Id": "acoustid_id", u"Acoustid Id": "acoustid_id",
}): }.items():
EasyID3.RegisterTXXXKey(key, desc) EasyID3.RegisterTXXXKey(key, desc)
Executable → Regular
+17 -21
View File
@@ -9,7 +9,6 @@
from mutagen import Tags from mutagen import Tags
from mutagen._util import DictMixin, dict_match from mutagen._util import DictMixin, dict_match
from mutagen.mp4 import MP4, MP4Tags, error, delete from mutagen.mp4 import MP4, MP4Tags, error, delete
from ._compat import PY2, text_type, PY3
__all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"] __all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"]
@@ -42,11 +41,14 @@ class EasyMP4Tags(DictMixin, Tags):
self.load = self.__mp4.load self.load = self.__mp4.load
self.save = self.__mp4.save self.save = self.__mp4.save
self.delete = self.__mp4.delete self.delete = self.__mp4.delete
self._padding = self.__mp4._padding
filename = property(lambda s: s.__mp4.filename, filename = property(lambda s: s.__mp4.filename,
lambda s, fn: setattr(s.__mp4, 'filename', fn)) lambda s, fn: setattr(s.__mp4, 'filename', fn))
@property
def _padding(self):
return self.__mp4._padding
@classmethod @classmethod
def RegisterKey(cls, key, def RegisterKey(cls, key,
getter=None, setter=None, deleter=None, lister=None): getter=None, setter=None, deleter=None, lister=None):
@@ -103,7 +105,7 @@ class EasyMP4Tags(DictMixin, Tags):
""" """
def getter(tags, key): def getter(tags, key):
return list(map(text_type, tags[atomid])) return list(map(str, tags[atomid]))
def setter(tags, key, value): def setter(tags, key, value):
clamp = lambda x: int(min(max(min_value, x), max_value)) clamp = lambda x: int(min(max(min_value, x), max_value))
@@ -121,9 +123,9 @@ class EasyMP4Tags(DictMixin, Tags):
ret = [] ret = []
for (track, total) in tags[atomid]: for (track, total) in tags[atomid]:
if total: if total:
ret.append("%d/%d" % (track, total)) ret.append(u"%d/%d" % (track, total))
else: else:
ret.append(text_type(track)) ret.append(str(track))
return ret return ret
def setter(tags, key, value): def setter(tags, key, value):
@@ -164,10 +166,8 @@ class EasyMP4Tags(DictMixin, Tags):
def setter(tags, key, value): def setter(tags, key, value):
encoded = [] encoded = []
for v in value: for v in value:
if not isinstance(v, text_type): if not isinstance(v, str):
if PY3:
raise TypeError("%r not str" % v) raise TypeError("%r not str" % v)
v = v.decode("utf-8")
encoded.append(v.encode("utf-8")) encoded.append(v.encode("utf-8"))
tags[atomid] = encoded tags[atomid] = encoded
@@ -187,12 +187,8 @@ class EasyMP4Tags(DictMixin, Tags):
def __setitem__(self, key, value): def __setitem__(self, key, value):
key = key.lower() key = key.lower()
if PY2:
if isinstance(value, str): if isinstance(value, str):
value = [value] value = [value]
else:
if isinstance(value, text_type):
value = [value]
func = dict_match(self.Set, key) func = dict_match(self.Set, key)
if func is not None: if func is not None:
@@ -210,7 +206,7 @@ class EasyMP4Tags(DictMixin, Tags):
def keys(self): def keys(self):
keys = [] keys = []
for key in list(self.Get.keys()): for key in self.Get.keys():
if key in self.List: if key in self.List:
keys.extend(self.List[key](self.__mp4, key)) keys.extend(self.List[key](self.__mp4, key))
elif key in self: elif key in self:
@@ -226,7 +222,7 @@ class EasyMP4Tags(DictMixin, Tags):
strings.append("%s=%s" % (key, value)) strings.append("%s=%s" % (key, value))
return "\n".join(strings) return "\n".join(strings)
for atomid, key in list({ for atomid, key in {
'\xa9nam': 'title', '\xa9nam': 'title',
'\xa9alb': 'album', '\xa9alb': 'album',
'\xa9ART': 'artist', '\xa9ART': 'artist',
@@ -242,10 +238,10 @@ for atomid, key in list({
'soar': 'artistsort', 'soar': 'artistsort',
'sonm': 'titlesort', 'sonm': 'titlesort',
'soco': 'composersort', 'soco': 'composersort',
}.items()): }.items():
EasyMP4Tags.RegisterTextKey(key, atomid) EasyMP4Tags.RegisterTextKey(key, atomid)
for name, key in list({ for name, key in {
'MusicBrainz Artist Id': 'musicbrainz_artistid', 'MusicBrainz Artist Id': 'musicbrainz_artistid',
'MusicBrainz Track Id': 'musicbrainz_trackid', 'MusicBrainz Track Id': 'musicbrainz_trackid',
'MusicBrainz Album Id': 'musicbrainz_albumid', 'MusicBrainz Album Id': 'musicbrainz_albumid',
@@ -254,18 +250,18 @@ for name, key in list({
'MusicBrainz Album Status': 'musicbrainz_albumstatus', 'MusicBrainz Album Status': 'musicbrainz_albumstatus',
'MusicBrainz Album Type': 'musicbrainz_albumtype', 'MusicBrainz Album Type': 'musicbrainz_albumtype',
'MusicBrainz Release Country': 'releasecountry', 'MusicBrainz Release Country': 'releasecountry',
}.items()): }.items():
EasyMP4Tags.RegisterFreeformKey(key, name) EasyMP4Tags.RegisterFreeformKey(key, name)
for name, key in list({ for name, key in {
"tmpo": "bpm", "tmpo": "bpm",
}.items()): }.items():
EasyMP4Tags.RegisterIntKey(key, name) EasyMP4Tags.RegisterIntKey(key, name)
for name, key in list({ for name, key in {
"trkn": "tracknumber", "trkn": "tracknumber",
"disk": "discnumber", "disk": "discnumber",
}.items()): }.items():
EasyMP4Tags.RegisterIntPairKey(key, name) EasyMP4Tags.RegisterIntPairKey(key, name)
Executable → Regular
+39 -34
View File
@@ -23,12 +23,12 @@ http://flac.sourceforge.net/format.html
__all__ = ["FLAC", "Open", "delete"] __all__ = ["FLAC", "Open", "delete"]
import struct import struct
from io import BytesIO
from ._vorbis import VCommentDict from ._vorbis import VCommentDict
import mutagen import mutagen
from ._compat import cBytesIO, endswith, chr_, xrange
from mutagen._util import resize_bytes, MutagenError, get_size, loadfile, \ from mutagen._util import resize_bytes, MutagenError, get_size, loadfile, \
convert_error convert_error, bchr, endswith
from mutagen._tags import PaddingInfo from mutagen._tags import PaddingInfo
from mutagen.id3._util import BitPaddedInt from mutagen.id3._util import BitPaddedInt
from functools import reduce from functools import reduce
@@ -101,7 +101,7 @@ class MetadataBlock(object):
if data is not None: if data is not None:
if not isinstance(data, StrictFileObject): if not isinstance(data, StrictFileObject):
if isinstance(data, bytes): if isinstance(data, bytes):
data = cBytesIO(data) data = BytesIO(data)
elif not hasattr(data, 'read'): elif not hasattr(data, 'read'):
raise TypeError( raise TypeError(
"StreamInfo requires string data or a file-like") "StreamInfo requires string data or a file-like")
@@ -201,7 +201,7 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo):
self.channels == other.channels and self.channels == other.channels and
self.bits_per_sample == other.bits_per_sample and self.bits_per_sample == other.bits_per_sample and
self.total_samples == other.total_samples) self.total_samples == other.total_samples)
except: except Exception:
return False return False
__hash__ = MetadataBlock.__hash__ __hash__ = MetadataBlock.__hash__
@@ -232,7 +232,7 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo):
self.md5_signature = to_int_be(data.read(16)) self.md5_signature = to_int_be(data.read(16))
def write(self): def write(self):
f = cBytesIO() f = BytesIO()
f.write(struct.pack(">I", self.min_blocksize)[-2:]) f.write(struct.pack(">I", self.min_blocksize)[-2:])
f.write(struct.pack(">I", self.max_blocksize)[-2:]) f.write(struct.pack(">I", self.max_blocksize)[-2:])
f.write(struct.pack(">I", self.min_framesize)[-3:]) f.write(struct.pack(">I", self.min_framesize)[-3:])
@@ -244,11 +244,11 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo):
byte = (self.sample_rate & 0xF) << 4 byte = (self.sample_rate & 0xF) << 4
byte += ((self.channels - 1) & 7) << 1 byte += ((self.channels - 1) & 7) << 1
byte += ((self.bits_per_sample - 1) >> 4) & 1 byte += ((self.bits_per_sample - 1) >> 4) & 1
f.write(chr_(byte)) f.write(bchr(byte))
# 4 bits of bps, 4 of sample count # 4 bits of bps, 4 of sample count
byte = ((self.bits_per_sample - 1) & 0xF) << 4 byte = ((self.bits_per_sample - 1) & 0xF) << 4
byte += (self.total_samples >> 32) & 0xF byte += (self.total_samples >> 32) & 0xF
f.write(chr_(byte)) f.write(bchr(byte))
# last 32 of sample count # last 32 of sample count
f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFF)) f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFF))
# MD5 signature # MD5 signature
@@ -259,7 +259,7 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo):
return f.getvalue() return f.getvalue()
def pprint(self): def pprint(self):
return "FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate) return u"FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate)
class SeekPoint(tuple): class SeekPoint(tuple):
@@ -284,6 +284,9 @@ class SeekPoint(tuple):
return super(cls, SeekPoint).__new__( return super(cls, SeekPoint).__new__(
cls, (first_sample, byte_offset, num_samples)) cls, (first_sample, byte_offset, num_samples))
def __getnewargs__(self):
return self.first_sample, self.byte_offset, self.num_samples
first_sample = property(lambda self: self[0]) first_sample = property(lambda self: self[0])
byte_offset = property(lambda self: self[1]) byte_offset = property(lambda self: self[1])
num_samples = property(lambda self: self[2]) num_samples = property(lambda self: self[2])
@@ -322,7 +325,7 @@ class SeekTable(MetadataBlock):
sp = data.tryread(self.__SEEKPOINT_SIZE) sp = data.tryread(self.__SEEKPOINT_SIZE)
def write(self): def write(self):
f = cBytesIO() f = BytesIO()
for seekpoint in self.seekpoints: for seekpoint in self.seekpoints:
packed = struct.pack( packed = struct.pack(
self.__SEEKPOINT_FORMAT, self.__SEEKPOINT_FORMAT,
@@ -391,10 +394,10 @@ class CueSheetTrack(object):
Attributes: Attributes:
track_number (`int`): track number track_number (`int`): track number
start_offset (`int`): track offset in samples from start of FLAC stream start_offset (`int`): track offset in samples from start of FLAC stream
isrc (`text`): ISRC code, exactly 12 characters isrc (`mutagen.text`): ISRC code, exactly 12 characters
type (`int`): 0 for audio, 1 for digital data type (`int`): 0 for audio, 1 for digital data
pre_emphasis (`bool`): true if the track is recorded with pre-emphasis pre_emphasis (`bool`): true if the track is recorded with pre-emphasis
indexes (List[`mutagen.flac.CueSheetTrackIndex`]): indexes (list[CueSheetTrackIndex]):
list of CueSheetTrackIndex objects list of CueSheetTrackIndex objects
""" """
@@ -437,14 +440,14 @@ class CueSheet(MetadataBlock):
in the cue sheet. in the cue sheet.
Attributes: Attributes:
media_catalog_number (`text`): media catalog number in ASCII, media_catalog_number (`mutagen.text`): media catalog number in ASCII,
up to 128 characters up to 128 characters
lead_in_samples (`int`): number of lead-in samples lead_in_samples (`int`): number of lead-in samples
compact_disc (`bool`): true if the cuesheet corresponds to a compact_disc (`bool`): true if the cuesheet corresponds to a
compact disc compact disc
tracks (List[`mutagen.flac.CueSheetTrack`]): tracks (list[CueSheetTrack]):
list of CueSheetTrack objects list of CueSheetTrack objects
lead_out (`mutagen.flac.CueSheetTrack` or `None`): lead_out (`CueSheetTrack` or `None`):
lead-out as CueSheetTrack or None if lead-out was not found lead-out as CueSheetTrack or None if lead-out was not found
""" """
@@ -502,7 +505,7 @@ class CueSheet(MetadataBlock):
self.tracks.append(val) self.tracks.append(val)
def write(self): def write(self):
f = cBytesIO() f = BytesIO()
flags = 0 flags = 0
if self.compact_disc: if self.compact_disc:
flags |= 0x80 flags |= 0x80
@@ -574,8 +577,8 @@ class Picture(MetadataBlock):
def __init__(self, data=None): def __init__(self, data=None):
self.type = 0 self.type = 0
self.mime = '' self.mime = u''
self.desc = '' self.desc = u''
self.width = 0 self.width = 0
self.height = 0 self.height = 0
self.depth = 0 self.depth = 0
@@ -608,7 +611,7 @@ class Picture(MetadataBlock):
self.data = data.read(length) self.data = data.read(length)
def write(self): def write(self):
f = cBytesIO() f = BytesIO()
mime = self.mime.encode('UTF-8') mime = self.mime.encode('UTF-8')
f.write(struct.pack('>2I', self.type, len(mime))) f.write(struct.pack('>2I', self.type, len(mime)))
f.write(mime) f.write(mime)
@@ -678,7 +681,7 @@ class FLAC(mutagen.FileType):
Attributes: Attributes:
cuesheet (`CueSheet`): if any or `None` cuesheet (`CueSheet`): if any or `None`
seektable (`SeekTable`): if any or `None` seektable (`SeekTable`): if any or `None`
pictures (List[`Picture`]): list of embedded pictures pictures (list[Picture]): list of embedded pictures
info (`StreamInfo`) info (`StreamInfo`)
tags (`mutagen._vorbis.VCommentDict`) tags (`mutagen._vorbis.VCommentDict`)
""" """
@@ -732,7 +735,9 @@ class FLAC(mutagen.FileType):
if self.tags is None: if self.tags is None:
self.tags = block self.tags = block
else: else:
raise FLACVorbisError("> 1 Vorbis comment block found") # https://github.com/quodlibet/mutagen/issues/377
# Something writes multiple and metaflac doesn't care
pass
elif block.code == CueSheet.code: elif block.code == CueSheet.code:
if self.cuesheet is None: if self.cuesheet is None:
self.cuesheet = block self.cuesheet = block
@@ -756,19 +761,21 @@ class FLAC(mutagen.FileType):
add_vorbiscomment = add_tags add_vorbiscomment = add_tags
@convert_error(IOError, error)
@loadfile(writable=True) @loadfile(writable=True)
def delete(self, filething): def delete(self, filething=None):
"""Remove Vorbis comments from a file. """Remove Vorbis comments from a file.
If no filename is given, the one most recently loaded is used. If no filename is given, the one most recently loaded is used.
""" """
if self.tags is not None: if self.tags is not None:
self.metadata_blocks.remove(self.tags) temp_blocks = [
try: b for b in self.metadata_blocks if b.code != VCFLACDict.code]
self.save(filething, padding=lambda x: 0) self._save(filething, temp_blocks, False, padding=lambda x: 0)
finally: self.metadata_blocks[:] = [
self.metadata_blocks.append(self.tags) b for b in self.metadata_blocks
if b.code != VCFLACDict.code or b is self.tags]
self.tags.clear() self.tags.clear()
vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.") vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.")
@@ -823,26 +830,24 @@ class FLAC(mutagen.FileType):
@property @property
def pictures(self): def pictures(self):
"""
Returns:
List[`Picture`]: List of embedded pictures
"""
return [b for b in self.metadata_blocks if b.code == Picture.code] return [b for b in self.metadata_blocks if b.code == Picture.code]
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile(writable=True) @loadfile(writable=True)
def save(self, filething, deleteid3=False, padding=None): def save(self, filething=None, deleteid3=False, padding=None):
"""Save metadata blocks to a file. """Save metadata blocks to a file.
Args: Args:
filething (filething) filething (filething)
deleteid3 (bool): delete id3 tags while at it deleteid3 (bool): delete id3 tags while at it
padding (PaddingFunction) padding (:obj:`mutagen.PaddingFunction`)
If no filename is given, the one most recently loaded is used. If no filename is given, the one most recently loaded is used.
""" """
self._save(filething, self.metadata_blocks, deleteid3, padding)
def _save(self, filething, metadata_blocks, deleteid3, padding):
f = StrictFileObject(filething.fileobj) f = StrictFileObject(filething.fileobj)
header = self.__check_header(f, filething.name) header = self.__check_header(f, filething.name)
audio_offset = self.__find_audio_offset(f) audio_offset = self.__find_audio_offset(f)
@@ -857,7 +862,7 @@ class FLAC(mutagen.FileType):
content_size = get_size(f) - audio_offset content_size = get_size(f) - audio_offset
assert content_size >= 0 assert content_size >= 0
data = MetadataBlock._writeblocks( data = MetadataBlock._writeblocks(
self.metadata_blocks, available, content_size, padding) metadata_blocks, available, content_size, padding)
data_size = len(data) data_size = len(data)
resize_bytes(filething.fileobj, available, data_size, header) resize_bytes(filething.fileobj, available, data_size, header)
Executable → Regular
+29 -7
View File
@@ -38,22 +38,30 @@ from ._frames import Frames, Frames_2_2, Frame, TextFrame, UrlFrame, \
from ._util import ID3NoHeaderError, error, ID3UnsupportedVersionError from ._util import ID3NoHeaderError, error, ID3UnsupportedVersionError
from ._id3v1 import ParseID3v1, MakeID3v1 from ._id3v1 import ParseID3v1, MakeID3v1
from ._tags import ID3Tags from ._tags import ID3Tags
from ._frames import (AENC, APIC, ASPI, BUF, CHAP, CNT, COM, COMM, COMR, CRA,
CRM, CTOC, ENCR, EQU2, ETC, ETCO, GEO, GEOB, GP1, GRID, GRP1, IPL, IPLS,
LINK, LNK, MCDI, MCI, MLL, MLLT, MVI, MVIN, MVN, MVNM, OWNE, PCNT, PCST,
PIC, POP, POPM, POSS, PRIV, RBUF, REV, RVA, RVA2, RVAD, RVRB, SEEK, SIGN,
SLT, STC, SYLT, SYTC, TAL, TALB, TBP, TBPM, TCAT, TCM, TCMP, TCO, TCOM,
TCON, TCOP, TCP, TCR, TDA, TDAT, TDEN, TDES, TDLY, TDOR, TDRC, TDRL, TDTG,
TDY, TEN, TENC, TEXT, TFLT, TFT, TGID, TIM, TIME, TIPL, TIT1, TIT2, TIT3,
TKE, TKEY, TKWD, TLA, TLAN, TLE, TLEN, TMCL, TMED, TMOO, TMT, TOA, TOAL,
TOF, TOFN, TOL, TOLY, TOPE, TOR, TORY, TOT, TOWN, TP1, TP2, TP3, TP4, TPA,
TPB, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB, TRC, TRCK, TRD, TRDA, TRK,
TRSN, TRSO, TS2, TSA, TSC, TSI, TSIZ, TSO2, TSOA, TSOC, TSOP, TSOT, TSP,
TSRC, TSS, TSSE, TSST, TST, TT1, TT2, TT3, TXT, TXX, TXXX, TYE, TYER, UFI,
UFID, ULT, USER, USLT, WAF, WAR, WAS, WCM, WCOM, WCOP, WCP, WFED, WOAF,
WOAR, WOAS, WORS, WPAY, WPB, WPUB, WXX, WXXX)
# deprecated # deprecated
from ._util import ID3EncryptionUnsupportedError, ID3JunkFrameError, \ from ._util import ID3EncryptionUnsupportedError, ID3JunkFrameError, \
ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, ID3Warning, \ ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, ID3Warning, \
BitPaddedInt as _BitPaddedIntForPicard BitPaddedInt as _BitPaddedIntForPicard
for f in Frames:
globals()[f] = Frames[f]
for f in Frames_2_2:
globals()[f] = Frames_2_2[f]
# support open(filename) as interface # support open(filename) as interface
Open = ID3 Open = ID3
# pyflakes # flake8
ID3, ID3FileType, delete, ID3v1SaveOptions, Encoding, PictureType, CTOCFlags, ID3, ID3FileType, delete, ID3v1SaveOptions, Encoding, PictureType, CTOCFlags,
ID3TimeStamp, Frames, Frames_2_2, Frame, TextFrame, UrlFrame, UrlFrameU, ID3TimeStamp, Frames, Frames_2_2, Frame, TextFrame, UrlFrame, UrlFrameU,
TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, NumericTextFrame, TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, NumericTextFrame,
@@ -62,6 +70,20 @@ ParseID3v1, MakeID3v1, ID3Tags, ID3EncryptionUnsupportedError,
ID3JunkFrameError, ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, ID3JunkFrameError, ID3BadUnsynchData, ID3BadCompressedData, ID3TagError,
ID3Warning ID3Warning
AENC, APIC, ASPI, BUF, CHAP, CNT, COM, COMM, COMR, CRA, CRM, CTOC, ENCR, EQU2,
ETC, ETCO, GEO, GEOB, GP1, GRID, GRP1, IPL, IPLS, LINK, LNK, MCDI, MCI, MLL,
MLLT, MVI, MVIN, MVN, MVNM, OWNE, PCNT, PCST, PIC, POP, POPM, POSS, PRIV,
RBUF, REV, RVA, RVA2, RVAD, RVRB, SEEK, SIGN, SLT, STC, SYLT, SYTC, TAL, TALB,
TBP, TBPM, TCAT, TCM, TCMP, TCO, TCOM, TCON, TCOP, TCP, TCR, TDA, TDAT, TDEN,
TDES, TDLY, TDOR, TDRC, TDRL, TDTG, TDY, TEN, TENC, TEXT, TFLT, TFT, TGID,
TIM, TIME, TIPL, TIT1, TIT2, TIT3, TKE, TKEY, TKWD, TLA, TLAN, TLE, TLEN,
TMCL, TMED, TMOO, TMT, TOA, TOAL, TOF, TOFN, TOL, TOLY, TOPE, TOR, TORY, TOT,
TOWN, TP1, TP2, TP3, TP4, TPA, TPB, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB,
TRC, TRCK, TRD, TRDA, TRK, TRSN, TRSO, TS2, TSA, TSC, TSI, TSIZ, TSO2, TSOA,
TSOC, TSOP, TSOT, TSP, TSRC, TSS, TSSE, TSST, TST, TT1, TT2, TT3, TXT, TXX,
TXXX, TYE, TYER, UFI, UFID, ULT, USER, USLT, WAF, WAR, WAS, WCM, WCOM, WCOP,
WCP, WFED, WOAF, WOAR, WOAS, WORS, WPAY, WPB, WPUB, WXX, WXXX
# Workaround for http://tickets.musicbrainz.org/browse/PICARD-833 # Workaround for http://tickets.musicbrainz.org/browse/PICARD-833
class _DummySpecForPicard(object): class _DummySpecForPicard(object):
Executable → Regular
+30 -15
View File
@@ -53,8 +53,8 @@ class ID3(ID3Tags, mutagen.Metadata):
filething (filething): or `None` filething (filething): or `None`
Attributes: Attributes:
version (Tuple[int]): ID3 tag version as a tuple version (tuple[int]): ID3 tag version as a tuple
unknown_frames (List[bytes]): raw frame data of any unknown frames unknown_frames (list[bytes]): raw frame data of any unknown frames
found found
size (int): the total size of the ID3 tag, including the header size (int): the total size of the ID3 tag, including the header
""" """
@@ -78,8 +78,6 @@ class ID3(ID3Tags, mutagen.Metadata):
@property @property
def version(self): def version(self):
"""`tuple`: ID3 tag version as a tuple (of the loaded file)"""
if self._header is not None: if self._header is not None:
return self._header.version return self._header.version
return self._version return self._version
@@ -112,10 +110,9 @@ class ID3(ID3Tags, mutagen.Metadata):
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile() @loadfile()
def load(self, filething, known_frames=None, translate=True, v2_version=4): def load(self, filething, known_frames=None, translate=True, v2_version=4,
"""load(filething, known_frames=None, translate=True, v2_version=4) load_v1=True):
"""Load tags from a filename.
Load tags from a filename.
Args: Args:
filename (filething): filename or file object to load tag data from filename (filething): filename or file object to load tag data from
@@ -126,6 +123,11 @@ class ID3(ID3Tags, mutagen.Metadata):
call update_to_v23() / update_to_v24() manually. call update_to_v23() / update_to_v24() manually.
v2_version (int): if update_to_v23 or update_to_v24 get called v2_version (int): if update_to_v23 or update_to_v24 get called
(3 or 4) (3 or 4)
load_v1 (bool): Load tags from ID3v1 header if present. If both
ID3v1 and ID3v2 headers are present, combine the tags from
the two, with ID3v2 having precedence.
.. versionadded:: 1.42
Example of loading a custom frame:: Example of loading a custom frame::
@@ -149,12 +151,16 @@ class ID3(ID3Tags, mutagen.Metadata):
try: try:
self._header = ID3Header(fileobj) self._header = ID3Header(fileobj)
except (ID3NoHeaderError, ID3UnsupportedVersionError): except (ID3NoHeaderError, ID3UnsupportedVersionError):
frames, offset = find_id3v1(fileobj) if not load_v1:
raise
frames, offset = find_id3v1(fileobj, v2_version, known_frames)
if frames is None: if frames is None:
raise raise
self.version = ID3Header._V11 self.version = ID3Header._V11
for v in list(frames.values()): for v in frames.values():
if len(self.getall(v.HashKey)) == 0:
self.add(v) self.add(v)
else: else:
# XXX: attach to the header object so we have it in spec parsing.. # XXX: attach to the header object so we have it in spec parsing..
@@ -165,6 +171,14 @@ class ID3(ID3Tags, mutagen.Metadata):
remaining_data = self._read(self._header, data) remaining_data = self._read(self._header, data)
self._padding = len(remaining_data) self._padding = len(remaining_data)
if load_v1:
v1v2_ver = 4 if self.version[1] == 4 else 3
frames, offset = find_id3v1(fileobj, v1v2_ver, known_frames)
if frames:
for v in frames.values():
if len(self.getall(v.HashKey)) == 0:
self.add(v)
if translate: if translate:
if v2_version == 3: if v2_version == 3:
self.update_to_v23() self.update_to_v23()
@@ -204,13 +218,14 @@ class ID3(ID3Tags, mutagen.Metadata):
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile(writable=True, create=True) @loadfile(writable=True, create=True)
def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
padding=None):
"""save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)
Save changes to a file. Save changes to a file.
Args: Args:
filename (fspath): filething (filething):
Filename to save the tag to. If no filename is given, Filename to save the tag to. If no filename is given,
the one most recently loaded is used. the one most recently loaded is used.
v1 (ID3v1SaveOptions): v1 (ID3v1SaveOptions):
@@ -223,7 +238,7 @@ class ID3(ID3Tags, mutagen.Metadata):
the separator used to join multiple text values the separator used to join multiple text values
if v2_version == 3. Defaults to '/' but if it's None if v2_version == 3. Defaults to '/' but if it's None
will be the ID3v2v2.4 null separator. will be the ID3v2v2.4 null separator.
padding (PaddingFunction) padding (:obj:`mutagen.PaddingFunction`)
Raises: Raises:
mutagen.MutagenError mutagen.MutagenError
@@ -268,7 +283,7 @@ class ID3(ID3Tags, mutagen.Metadata):
f.truncate() f.truncate()
@loadfile(writable=True) @loadfile(writable=True)
def delete(self, filething, delete_v1=True, delete_v2=True): def delete(self, filething=None, delete_v1=True, delete_v2=True):
"""delete(filething=None, delete_v1=True, delete_v2=True) """delete(filething=None, delete_v1=True, delete_v2=True)
Remove tags from a file. Remove tags from a file.
@@ -352,7 +367,7 @@ class ID3FileType(mutagen.FileType):
@staticmethod @staticmethod
def pprint(): def pprint():
return "Unknown format with ID3 tag" return u"Unknown format with ID3 tag"
@staticmethod @staticmethod
def score(filename, fileobj, header_data): def score(filename, fileobj, header_data):
Executable → Regular
+60 -55
View File
@@ -18,8 +18,6 @@ from ._specs import BinaryDataSpec, StringSpec, Latin1TextSpec, \
KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, \ KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, \
EncodedNumericTextSpec, SpecError, PictureTypeSpec, ID3FramesSpec, \ EncodedNumericTextSpec, SpecError, PictureTypeSpec, ID3FramesSpec, \
Latin1TextListSpec, CTOCFlagsSpec, FrameIDSpec, RVASpec Latin1TextListSpec, CTOCFlagsSpec, FrameIDSpec, RVASpec
from .._compat import text_type, string_types, swap_to_string, iteritems, \
izip, itervalues
def _bytes2key(b): def _bytes2key(b):
@@ -265,7 +263,7 @@ class Frame(object):
if tflags & Frame.FLAG24_COMPRESS: if tflags & Frame.FLAG24_COMPRESS:
try: try:
data = zlib.decompress(data) data = zlib.decompress(data)
except zlib.error as err: except zlib.error:
# the initial mutagen that went out with QL 0.12 did not # the initial mutagen that went out with QL 0.12 did not
# write the 4 bytes of uncompressed size. Compensate. # write the 4 bytes of uncompressed size. Compensate.
data = datalen_bytes + data data = datalen_bytes + data
@@ -277,6 +275,8 @@ class Frame(object):
elif header.version >= header._V23: elif header.version >= header._V23:
if tflags & Frame.FLAG23_COMPRESS: if tflags & Frame.FLAG23_COMPRESS:
if len(data) < 4:
raise ID3JunkFrameError('frame too small: %r' % data)
usize, = unpack('>L', data[:4]) usize, = unpack('>L', data[:4])
data = data[4:] data = data[4:]
if tflags & Frame.FLAG23_ENCRYPT: if tflags & Frame.FLAG23_ENCRYPT:
@@ -329,11 +329,11 @@ class CHAP(Frame):
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
def _pprint(self): def _pprint(self):
frame_pprint = "" frame_pprint = u""
for frame in itervalues(self.sub_frames): for frame in self.sub_frames.values():
for line in frame.pprint().splitlines(): for line in frame.pprint().splitlines():
frame_pprint += "\n" + " " * 4 + line frame_pprint += "\n" + " " * 4 + line
return "%s time=%d..%d offset=%d..%d%s" % ( return u"%s time=%d..%d offset=%d..%d%s" % (
self.element_id, self.start_time, self.end_time, self.element_id, self.start_time, self.end_time,
self.start_offset, self.end_offset, frame_pprint) self.start_offset, self.end_offset, frame_pprint)
@@ -368,16 +368,15 @@ class CTOC(Frame):
self.child_element_ids == other.child_element_ids self.child_element_ids == other.child_element_ids
def _pprint(self): def _pprint(self):
frame_pprint = "" frame_pprint = u""
if getattr(self, "sub_frames", None): if getattr(self, "sub_frames", None):
frame_pprint += "\n" + "\n".join( frame_pprint += "\n" + "\n".join(
[" " * 4 + f.pprint() for f in list(self.sub_frames.values())]) [" " * 4 + f.pprint() for f in self.sub_frames.values()])
return "%s flags=%d child_element_ids=%s%s" % ( return u"%s flags=%d child_element_ids=%s%s" % (
self.element_id, int(self.flags), self.element_id, int(self.flags),
",".join(self.child_element_ids), frame_pprint) u",".join(self.child_element_ids), frame_pprint)
@swap_to_string
class TextFrame(Frame): class TextFrame(Frame):
"""Text strings. """Text strings.
@@ -395,20 +394,20 @@ class TextFrame(Frame):
_framespec = [ _framespec = [
EncodingSpec('encoding', default=Encoding.UTF16), EncodingSpec('encoding', default=Encoding.UTF16),
MultiSpec('text', EncodedTextSpec('text'), sep='\u0000', default=[]), MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
] ]
def __bytes__(self): def __bytes__(self):
return text_type(self).encode('utf-8') return str(self).encode('utf-8')
def __str__(self): def __str__(self):
return '\u0000'.join(self.text) return u'\u0000'.join(self.text)
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, bytes): if isinstance(other, bytes):
return bytes(self) == other return bytes(self) == other
elif isinstance(other, text_type): elif isinstance(other, str):
return text_type(self) == other return str(self) == other
return self.text == other return self.text == other
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
@@ -434,6 +433,7 @@ class TextFrame(Frame):
for val in other[:]: for val in other[:]:
if val not in self: if val not in self:
self.append(val) self.append(val)
self.encoding = max(self.encoding, other.encoding)
return self return self
def _pprint(self): def _pprint(self):
@@ -451,7 +451,7 @@ class NumericTextFrame(TextFrame):
_framespec = [ _framespec = [
EncodingSpec('encoding', default=Encoding.UTF16), EncodingSpec('encoding', default=Encoding.UTF16),
MultiSpec('text', EncodedNumericTextSpec('text'), sep='\u0000', MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000',
default=[]), default=[]),
] ]
@@ -472,7 +472,7 @@ class NumericPartTextFrame(TextFrame):
_framespec = [ _framespec = [
EncodingSpec('encoding', default=Encoding.UTF16), EncodingSpec('encoding', default=Encoding.UTF16),
MultiSpec('text', EncodedNumericPartTextSpec('text'), sep='\u0000', MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000',
default=[]), default=[]),
] ]
@@ -480,7 +480,6 @@ class NumericPartTextFrame(TextFrame):
return int(self.text[0].split("/")[0]) return int(self.text[0].split("/")[0])
@swap_to_string
class TimeStampTextFrame(TextFrame): class TimeStampTextFrame(TextFrame):
"""A list of time stamps. """A list of time stamps.
@@ -490,20 +489,19 @@ class TimeStampTextFrame(TextFrame):
_framespec = [ _framespec = [
EncodingSpec('encoding', default=Encoding.UTF16), EncodingSpec('encoding', default=Encoding.UTF16),
MultiSpec('text', TimeStampSpec('stamp'), sep=',', default=[]), MultiSpec('text', TimeStampSpec('stamp'), sep=u',', default=[]),
] ]
def __bytes__(self): def __bytes__(self):
return text_type(self).encode('utf-8') return str(self).encode('utf-8')
def __str__(self): def __str__(self):
return ','.join([stamp.text for stamp in self.text]) return u','.join([stamp.text for stamp in self.text])
def _pprint(self): def _pprint(self):
return " / ".join([stamp.text for stamp in self.text]) return u" / ".join([stamp.text for stamp in self.text])
@swap_to_string
class UrlFrame(Frame): class UrlFrame(Frame):
"""A frame containing a URL string. """A frame containing a URL string.
@@ -574,11 +572,11 @@ class TCON(TextFrame):
try: try:
genres.append(self.GENRES[int(value)]) genres.append(self.GENRES[int(value)])
except IndexError: except IndexError:
genres.append("Unknown") genres.append(u"Unknown")
elif value == "CR": elif value == "CR":
genres.append("Cover") genres.append(u"Cover")
elif value == "RX": elif value == "RX":
genres.append("Remix") genres.append(u"Remix")
elif value: elif value:
newgenres = [] newgenres = []
genreid, dummy, genrename = genre_re.match(value).groups() genreid, dummy, genrename = genre_re.match(value).groups()
@@ -586,14 +584,14 @@ class TCON(TextFrame):
if genreid: if genreid:
for gid in genreid[1:-1].split(")("): for gid in genreid[1:-1].split(")("):
if gid.isdigit() and int(gid) < len(self.GENRES): if gid.isdigit() and int(gid) < len(self.GENRES):
gid = text_type(self.GENRES[int(gid)]) gid = str(self.GENRES[int(gid)])
newgenres.append(gid) newgenres.append(gid)
elif gid == "CR": elif gid == "CR":
newgenres.append("Cover") newgenres.append(u"Cover")
elif gid == "RX": elif gid == "RX":
newgenres.append("Remix") newgenres.append(u"Remix")
else: else:
newgenres.append("Unknown") newgenres.append(u"Unknown")
if genrename: if genrename:
# "Unescaping" the first parenthesis # "Unescaping" the first parenthesis
@@ -607,7 +605,7 @@ class TCON(TextFrame):
return genres return genres
def __set_genres(self, genres): def __set_genres(self, genres):
if isinstance(genres, string_types): if isinstance(genres, str):
genres = [genres] genres = [genres]
self.text = [self.__decode(g) for g in genres] self.text = [self.__decode(g) for g in genres]
@@ -669,6 +667,14 @@ class MVI(MVIN):
"iTunes Movement Number/Count" "iTunes Movement Number/Count"
class GRP1(TextFrame):
"iTunes Grouping"
class GP1(GRP1):
"iTunes Grouping"
class TDOR(TimeStampTextFrame): class TDOR(TimeStampTextFrame):
"Original Release Time" "Original Release Time"
@@ -860,7 +866,7 @@ class TXXX(TextFrame):
_framespec = [ _framespec = [
EncodingSpec('encoding'), EncodingSpec('encoding'),
EncodedTextSpec('desc'), EncodedTextSpec('desc'),
MultiSpec('text', EncodedTextSpec('text'), sep='\u0000', default=[]), MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
] ]
@property @property
@@ -1035,7 +1041,6 @@ class SYTC(Frame):
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
@swap_to_string
class USLT(Frame): class USLT(Frame):
"""Unsynchronised lyrics/text transcription. """Unsynchronised lyrics/text transcription.
@@ -1045,7 +1050,7 @@ class USLT(Frame):
_framespec = [ _framespec = [
EncodingSpec('encoding', default=Encoding.UTF16), EncodingSpec('encoding', default=Encoding.UTF16),
StringSpec('lang', length=3, default="XXX"), StringSpec('lang', length=3, default=u"XXX"),
EncodedTextSpec('desc'), EncodedTextSpec('desc'),
EncodedTextSpec('text'), EncodedTextSpec('text'),
] ]
@@ -1065,14 +1070,16 @@ class USLT(Frame):
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
def _pprint(self):
return "%s=%s=%s" % (self.desc, self.lang, self.text)
@swap_to_string
class SYLT(Frame): class SYLT(Frame):
"""Synchronised lyrics/text.""" """Synchronised lyrics/text."""
_framespec = [ _framespec = [
EncodingSpec('encoding'), EncodingSpec('encoding'),
StringSpec('lang', length=3, default="XXX"), StringSpec('lang', length=3, default=u"XXX"),
ByteSpec('format', default=1), ByteSpec('format', default=1),
ByteSpec('type', default=0), ByteSpec('type', default=0),
EncodedTextSpec('desc'), EncodedTextSpec('desc'),
@@ -1083,16 +1090,21 @@ class SYLT(Frame):
def HashKey(self): def HashKey(self):
return '%s:%s:%s' % (self.FrameID, self.desc, self.lang) return '%s:%s:%s' % (self.FrameID, self.desc, self.lang)
def _pprint(self):
return str(self)
def __eq__(self, other): def __eq__(self, other):
return str(self) == other return str(self) == other
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
def __str__(self): def __str__(self):
return "".join(text for (text, time) in self.text) unit = 'fr' if self.format == 1 else 'ms'
return u"\n".join("[{0}{1}]: {2}".format(time, unit, text)
for (text, time) in self.text)
def __bytes__(self): def __bytes__(self):
return text_type(self).encode("utf-8") return str(self).encode("utf-8")
class COMM(TextFrame): class COMM(TextFrame):
@@ -1106,7 +1118,7 @@ class COMM(TextFrame):
EncodingSpec('encoding'), EncodingSpec('encoding'),
StringSpec('lang', length=3, default="XXX"), StringSpec('lang', length=3, default="XXX"),
EncodedTextSpec('desc'), EncodedTextSpec('desc'),
MultiSpec('text', EncodedTextSpec('text'), sep='\u0000', default=[]), MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
] ]
@property @property
@@ -1263,11 +1275,11 @@ class APIC(Frame):
return '%s:%s' % (self.FrameID, self.desc) return '%s:%s' % (self.FrameID, self.desc)
def _merge_frame(self, other): def _merge_frame(self, other):
other.desc += " " other.desc += u" "
return other return other
def _pprint(self): def _pprint(self):
type_desc = text_type(self.type) type_desc = str(self.type)
if hasattr(self.type, "_pprint"): if hasattr(self.type, "_pprint"):
type_desc = self.type._pprint() type_desc = self.type._pprint()
@@ -1297,7 +1309,7 @@ class PCNT(Frame):
return self.count return self.count
def _pprint(self): def _pprint(self):
return text_type(self.count) return str(self.count)
class PCST(Frame): class PCST(Frame):
@@ -1316,7 +1328,7 @@ class PCST(Frame):
return self.value return self.value
def _pprint(self): def _pprint(self):
return text_type(self.value) return str(self.value)
class POPM(Frame): class POPM(Frame):
@@ -1420,7 +1432,6 @@ class RBUF(Frame):
return self.size return self.size
@swap_to_string
class AENC(Frame): class AENC(Frame):
"""Audio encryption. """Audio encryption.
@@ -1537,7 +1548,6 @@ class UFID(Frame):
return "%s=%r" % (self.owner, self.data) return "%s=%r" % (self.owner, self.data)
@swap_to_string
class USER(Frame): class USER(Frame):
"""Terms of use. """Terms of use.
@@ -1550,7 +1560,7 @@ class USER(Frame):
_framespec = [ _framespec = [
EncodingSpec('encoding'), EncodingSpec('encoding'),
StringSpec('lang', length=3, default="XXX"), StringSpec('lang', length=3, default=u"XXX"),
EncodedTextSpec('text'), EncodedTextSpec('text'),
] ]
@@ -1573,14 +1583,13 @@ class USER(Frame):
return "%r=%s" % (self.lang, self.text) return "%r=%s" % (self.lang, self.text)
@swap_to_string
class OWNE(Frame): class OWNE(Frame):
"""Ownership frame.""" """Ownership frame."""
_framespec = [ _framespec = [
EncodingSpec('encoding'), EncodingSpec('encoding'),
Latin1TextSpec('price'), Latin1TextSpec('price'),
StringSpec('date', length=8, default="19700101"), StringSpec('date', length=8, default=u"19700101"),
EncodedTextSpec('seller'), EncodedTextSpec('seller'),
] ]
@@ -1602,7 +1611,7 @@ class COMR(Frame):
_framespec = [ _framespec = [
EncodingSpec('encoding'), EncodingSpec('encoding'),
Latin1TextSpec('price'), Latin1TextSpec('price'),
StringSpec('valid_until', length=8, default="19700101"), StringSpec('valid_until', length=8, default=u"19700101"),
Latin1TextSpec('contact'), Latin1TextSpec('contact'),
ByteSpec('format', default=0), ByteSpec('format', default=0),
EncodedTextSpec('seller'), EncodedTextSpec('seller'),
@@ -1624,7 +1633,6 @@ class COMR(Frame):
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
@swap_to_string
class ENCR(Frame): class ENCR(Frame):
"""Encryption method registration. """Encryption method registration.
@@ -1651,7 +1659,6 @@ class ENCR(Frame):
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
@swap_to_string
class GRID(Frame): class GRID(Frame):
"""Group identification registration.""" """Group identification registration."""
@@ -1680,7 +1687,6 @@ class GRID(Frame):
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
@swap_to_string
class PRIV(Frame): class PRIV(Frame):
"""Private frame.""" """Private frame."""
@@ -1706,7 +1712,6 @@ class PRIV(Frame):
__hash__ = Frame.__hash__ __hash__ = Frame.__hash__
@swap_to_string
class SIGN(Frame): class SIGN(Frame):
"""Signature frame.""" """Signature frame."""
@@ -2118,7 +2123,7 @@ Frames_2_2 = {}
k, v = None, None k, v = None, None
for k, v in iteritems(globals()): for k, v in globals().items():
if isinstance(v, type) and issubclass(v, Frame): if isinstance(v, type) and issubclass(v, Frame):
v.__module__ = "mutagen.id3" v.__module__ = "mutagen.id3"
Executable → Regular
+67 -25
View File
@@ -11,22 +11,32 @@
import errno import errno
from struct import error as StructError, unpack from struct import error as StructError, unpack
from mutagen._util import chr_, text_type from mutagen._util import bchr
from ._frames import TCON, TRCK, COMM, TDRC, TALB, TPE1, TIT2 from ._frames import TCON, TRCK, COMM, TDRC, TYER, TALB, TPE1, TIT2
def find_id3v1(fileobj): def find_id3v1(fileobj, v2_version=4, known_frames=None):
"""Returns a tuple of (id3tag, offset_to_end) or (None, 0) """Returns a tuple of (id3tag, offset_to_end) or (None, 0)
offset mainly because we used to write too short tags in some cases and offset mainly because we used to write too short tags in some cases and
we need the offset to delete them. we need the offset to delete them.
v2_version: Decides whether ID3v2.3 or ID3v2.4 tags
should be returned. Must be 3 or 4.
known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame
IDs to Frame objects
""" """
if v2_version not in (3, 4):
raise ValueError("Only 3 and 4 possible for v2_version")
# id3v1 is always at the end (after apev2) # id3v1 is always at the end (after apev2)
extra_read = b"APETAGEX".index(b"TAG") extra_read = b"APETAGEX".index(b"TAG")
old_pos = fileobj.tell()
try: try:
fileobj.seek(-128 - extra_read, 2) fileobj.seek(-128 - extra_read, 2)
except IOError as e: except IOError as e:
@@ -38,6 +48,7 @@ def find_id3v1(fileobj):
raise raise
data = fileobj.read(128 + extra_read) data = fileobj.read(128 + extra_read)
fileobj.seek(old_pos, 0)
try: try:
idx = data.index(b"TAG") idx = data.index(b"TAG")
except ValueError: except ValueError:
@@ -53,7 +64,7 @@ def find_id3v1(fileobj):
if idx == ape_idx + extra_read: if idx == ape_idx + extra_read:
return (None, 0) return (None, 0)
tag = ParseID3v1(data[idx:]) tag = ParseID3v1(data[idx:], v2_version, known_frames)
if tag is None: if tag is None:
return (None, 0) return (None, 0)
@@ -62,12 +73,21 @@ def find_id3v1(fileobj):
# ID3v1.1 support. # ID3v1.1 support.
def ParseID3v1(data): def ParseID3v1(data, v2_version=4, known_frames=None):
"""Parse an ID3v1 tag, returning a list of ID3v2.4 frames. """Parse an ID3v1 tag, returning a list of ID3v2 frames
Returns a {frame_name: frame} dict or None. Returns a {frame_name: frame} dict or None.
v2_version: Decides whether ID3v2.3 or ID3v2.4 tags
should be returned. Must be 3 or 4.
known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame
IDs to Frame objects
""" """
if v2_version not in (3, 4):
raise ValueError("Only 3 and 4 possible for v2_version")
try: try:
data = data[data.index(b"TAG"):] data = data[data.index(b"TAG"):]
except ValueError: except ValueError:
@@ -94,26 +114,48 @@ def ParseID3v1(data):
def fix(data): def fix(data):
return data.split(b"\x00")[0].strip().decode('latin1') return data.split(b"\x00")[0].strip().decode('latin1')
title, artist, album, year, comment = list(map( title, artist, album, year, comment = map(
fix, [title, artist, album, year, comment])) fix, [title, artist, album, year, comment])
frame_class = {
"TIT2": TIT2,
"TPE1": TPE1,
"TALB": TALB,
"TYER": TYER,
"TDRC": TDRC,
"COMM": COMM,
"TRCK": TRCK,
"TCON": TCON,
}
for key in frame_class:
if known_frames is not None:
if key in known_frames:
frame_class[key] = known_frames[key]
else:
frame_class[key] = None
frames = {} frames = {}
if title: if title and frame_class["TIT2"]:
frames["TIT2"] = TIT2(encoding=0, text=title) frames["TIT2"] = frame_class["TIT2"](encoding=0, text=title)
if artist: if artist and frame_class["TPE1"]:
frames["TPE1"] = TPE1(encoding=0, text=[artist]) frames["TPE1"] = frame_class["TPE1"](encoding=0, text=[artist])
if album: if album and frame_class["TALB"]:
frames["TALB"] = TALB(encoding=0, text=album) frames["TALB"] = frame_class["TALB"](encoding=0, text=album)
if year: if year:
frames["TDRC"] = TDRC(encoding=0, text=year) if v2_version == 3 and frame_class["TYER"]:
if comment: frames["TYER"] = frame_class["TYER"](encoding=0, text=year)
frames["COMM"] = COMM( elif frame_class["TDRC"]:
frames["TDRC"] = frame_class["TDRC"](encoding=0, text=year)
if comment and frame_class["COMM"]:
frames["COMM"] = frame_class["COMM"](
encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) encoding=0, lang="eng", desc="ID3v1 Comment", text=comment)
# Don't read a track number if it looks like the comment was # Don't read a track number if it looks like the comment was
# padded with spaces instead of nulls (thanks, WinAmp). # padded with spaces instead of nulls (thanks, WinAmp).
if track and ((track != 32) or (data[-3] == b'\x00'[0])): if (track and frame_class["TRCK"] and
((track != 32) or (data[-3] == b'\x00'[0]))):
frames["TRCK"] = TRCK(encoding=0, text=str(track)) frames["TRCK"] = TRCK(encoding=0, text=str(track))
if genre != 255: if genre != 255 and frame_class["TCON"]:
frames["TCON"] = TCON(encoding=0, text=str(genre)) frames["TCON"] = TCON(encoding=0, text=str(genre))
return frames return frames
@@ -123,8 +165,8 @@ def MakeID3v1(id3):
v1 = {} v1 = {}
for v2id, name in list({"TIT2": "title", "TPE1": "artist", for v2id, name in {"TIT2": "title", "TPE1": "artist",
"TALB": "album"}.items()): "TALB": "album"}.items():
if v2id in id3: if v2id in id3:
text = id3[v2id].text[0].encode('latin1', 'replace')[:30] text = id3[v2id].text[0].encode('latin1', 'replace')[:30]
else: else:
@@ -139,7 +181,7 @@ def MakeID3v1(id3):
if "TRCK" in id3: if "TRCK" in id3:
try: try:
v1["track"] = chr_(+id3["TRCK"]) v1["track"] = bchr(+id3["TRCK"])
except ValueError: except ValueError:
v1["track"] = b"\x00" v1["track"] = b"\x00"
else: else:
@@ -152,14 +194,14 @@ def MakeID3v1(id3):
pass pass
else: else:
if genre in TCON.GENRES: if genre in TCON.GENRES:
v1["genre"] = chr_(TCON.GENRES.index(genre)) v1["genre"] = bchr(TCON.GENRES.index(genre))
if "genre" not in v1: if "genre" not in v1:
v1["genre"] = b"\xff" v1["genre"] = b"\xff"
if "TDRC" in id3: if "TDRC" in id3:
year = text_type(id3["TDRC"]).encode('ascii') year = str(id3["TDRC"]).encode('ascii')
elif "TYER" in id3: elif "TYER" in id3:
year = text_type(id3["TYER"]).encode('ascii') year = str(id3["TYER"]).encode('ascii')
else: else:
year = b"" year = b""
v1["year"] = (year + b"\x00\x00\x00\x00")[:4] v1["year"] = (year + b"\x00\x00\x00\x00")[:4]
Executable → Regular
+23 -32
View File
@@ -10,10 +10,8 @@ import struct
import codecs import codecs
from struct import unpack, pack from struct import unpack, pack
from .._compat import text_type, chr_, PY3, swap_to_string, string_types, \ from .._util import total_ordering, decode_terminated, enum, flags, \
xrange cdata, encode_endian, intround, bchr
from .._util import total_ordering, decode_terminated, enum, izip, flags, \
cdata, encode_endian
from ._util import BitPaddedInt, is_valid_frame_id from ._util import BitPaddedInt, is_valid_frame_id
@@ -87,7 +85,7 @@ class PictureType(object):
"""Publisher/Studio logotype""" """Publisher/Studio logotype"""
def _pprint(self): def _pprint(self):
return text_type(self).split(".", 1)[-1].lower().replace("_", " ") return str(self).split(".", 1)[-1].lower().replace("_", " ")
@flags @flags
@@ -165,11 +163,11 @@ class ByteSpec(Spec):
return bytearray(data)[0], data[1:] return bytearray(data)[0], data[1:]
def write(self, config, frame, value): def write(self, config, frame, value):
return chr_(value) return bchr(value)
def validate(self, frame, value): def validate(self, frame, value):
if value is not None: if value is not None:
chr_(value) bchr(value)
return value return value
@@ -278,7 +276,7 @@ class StringSpec(Spec):
def __init__(self, name, length, default=None): def __init__(self, name, length, default=None):
if default is None: if default is None:
default = " " * length default = u" " * length
super(StringSpec, self).__init__(name, default) super(StringSpec, self).__init__(name, default)
self.len = length self.len = length
@@ -289,26 +287,22 @@ class StringSpec(Spec):
except UnicodeDecodeError: except UnicodeDecodeError:
raise SpecError("not ascii") raise SpecError("not ascii")
else: else:
if PY3:
chunk = ascii chunk = ascii
return chunk, data[s.len:] return chunk, data[s.len:]
def write(self, config, frame, value): def write(self, config, frame, value):
if PY3:
value = value.encode("ascii") value = value.encode("ascii")
return (bytes(value) + b'\x00' * self.len)[:self.len] return (bytes(value) + b'\x00' * self.len)[:self.len]
def validate(self, frame, value): def validate(self, frame, value):
if value is None: if value is None:
raise TypeError raise TypeError
if PY3:
if not isinstance(value, str): if not isinstance(value, str):
raise TypeError("%s has to be str" % self.name) raise TypeError("%s has to be str" % self.name)
value.encode("ascii") value.encode("ascii")
else:
if not isinstance(value, bytes):
value = value.encode("ascii")
if len(value) == self.len: if len(value) == self.len:
return value return value
@@ -402,7 +396,7 @@ class RVASpec(Spec):
class FrameIDSpec(StringSpec): class FrameIDSpec(StringSpec):
def __init__(self, name, length): def __init__(self, name, length):
super(FrameIDSpec, self).__init__(name, length, "X" * length) super(FrameIDSpec, self).__init__(name, length, u"X" * length)
def validate(self, frame, value): def validate(self, frame, value):
value = super(FrameIDSpec, self).validate(frame, value) value = super(FrameIDSpec, self).validate(frame, value)
@@ -424,7 +418,7 @@ class BinaryDataSpec(Spec):
def write(self, config, frame, value): def write(self, config, frame, value):
if isinstance(value, bytes): if isinstance(value, bytes):
return value return value
value = text_type(value).encode("ascii") value = str(value).encode("ascii")
return value return value
def validate(self, frame, value): def validate(self, frame, value):
@@ -432,10 +426,10 @@ class BinaryDataSpec(Spec):
raise TypeError raise TypeError
if isinstance(value, bytes): if isinstance(value, bytes):
return value return value
elif PY3: else:
raise TypeError("%s has to be bytes" % self.name) raise TypeError("%s has to be bytes" % self.name)
value = text_type(value).encode("ascii") value = str(value).encode("ascii")
return value return value
@@ -464,7 +458,7 @@ class EncodedTextSpec(Spec):
Encoding.UTF8: ('utf8', b'\x00'), Encoding.UTF8: ('utf8', b'\x00'),
} }
def __init__(self, name, default=""): def __init__(self, name, default=u""):
super(EncodedTextSpec, self).__init__(name, default) super(EncodedTextSpec, self).__init__(name, default)
def read(self, header, frame, data): def read(self, header, frame, data):
@@ -493,7 +487,7 @@ class EncodedTextSpec(Spec):
raise SpecError(e) raise SpecError(e)
def validate(self, frame, value): def validate(self, frame, value):
return text_type(value) return str(value)
class MultiSpec(Spec): class MultiSpec(Spec):
@@ -527,7 +521,7 @@ class MultiSpec(Spec):
return b''.join(data) return b''.join(data)
def validate(self, frame, value): def validate(self, frame, value):
if self.sep and isinstance(value, string_types): if self.sep and isinstance(value, str):
value = value.split(self.sep) value = value.split(self.sep)
if isinstance(value, list): if isinstance(value, list):
if len(self.specs) == 1: if len(self.specs) == 1:
@@ -568,7 +562,7 @@ class EncodedNumericPartTextSpec(EncodedTextSpec):
class Latin1TextSpec(Spec): class Latin1TextSpec(Spec):
def __init__(self, name, default=""): def __init__(self, name, default=u""):
super(Latin1TextSpec, self).__init__(name, default) super(Latin1TextSpec, self).__init__(name, default)
def read(self, header, frame, data): def read(self, header, frame, data):
@@ -582,7 +576,7 @@ class Latin1TextSpec(Spec):
return value.encode('latin1') + b'\x00' return value.encode('latin1') + b'\x00'
def validate(self, frame, value): def validate(self, frame, value):
return text_type(value) return str(value)
class ID3FramesSpec(Spec): class ID3FramesSpec(Spec):
@@ -602,7 +596,7 @@ class ID3FramesSpec(Spec):
from ._tags import ID3Tags from ._tags import ID3Tags
v = ID3Tags() v = ID3Tags()
for frame in list(value.values()): for frame in value.values():
v.add(frame._get_v23_frame(**kwargs)) v.add(frame._get_v23_frame(**kwargs))
return v return v
@@ -647,7 +641,6 @@ class Latin1TextListSpec(Spec):
return [self._lspec.validate(frame, v) for v in value] return [self._lspec.validate(frame, v) for v in value]
@swap_to_string
@total_ordering @total_ordering
class ID3TimeStamp(object): class ID3TimeStamp(object):
"""A time stamp in ID3v2 format. """A time stamp in ID3v2 format.
@@ -665,10 +658,8 @@ class ID3TimeStamp(object):
def __init__(self, text): def __init__(self, text):
if isinstance(text, ID3TimeStamp): if isinstance(text, ID3TimeStamp):
text = text.text text = text.text
elif not isinstance(text, text_type): elif not isinstance(text, str):
if PY3:
raise TypeError("not a str") raise TypeError("not a str")
text = text.decode("utf-8")
self.text = text self.text = text
@@ -683,9 +674,9 @@ class ID3TimeStamp(object):
if part is None: if part is None:
break break
pieces.append(self.__formats[i] % part + self.__seps[i]) pieces.append(self.__formats[i] % part + self.__seps[i])
return ''.join(pieces)[:-1] return u''.join(pieces)[:-1]
def set_text(self, text, splitre=re.compile('[-T:/.]|\s+')): def set_text(self, text, splitre=re.compile('[-T:/.]|\\s+')):
year, month, day, hour, minute, second = \ year, month, day, hour, minute, second = \
splitre.split(text + ':::::')[:6] splitre.split(text + ':::::')[:6]
for a in 'year month day hour minute second'.split(): for a in 'year month day hour minute second'.split():
@@ -745,7 +736,7 @@ class VolumeAdjustmentSpec(Spec):
return value / 512.0, data[2:] return value / 512.0, data[2:]
def write(self, config, frame, value): def write(self, config, frame, value):
number = int(round(value * 512)) number = intround(value * 512)
# pack only fails in 2.7, do it manually in 2.6 # pack only fails in 2.7, do it manually in 2.6
if not -32768 <= number <= 32767: if not -32768 <= number <= 32767:
raise SpecError("not in range") raise SpecError("not in range")
@@ -778,7 +769,7 @@ class VolumePeakSpec(Spec):
return (float(peak) / (2 ** 31 - 1)), data[1 + vol_bytes:] return (float(peak) / (2 ** 31 - 1)), data[1 + vol_bytes:]
def write(self, config, frame, value): def write(self, config, frame, value):
number = int(round(value * 32768)) number = intround(value * 32768)
# pack only fails in 2.7, do it manually in 2.6 # pack only fails in 2.7, do it manually in 2.6
if not 0 <= number <= 65535: if not 0 <= number <= 65535:
raise SpecError("not in range") raise SpecError("not in range")
Executable → Regular
+24 -32
View File
@@ -7,11 +7,12 @@
# the Free Software Foundation; either version 2 of the License, or # the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version. # (at your option) any later version.
import re
import struct import struct
from itertools import zip_longest
from mutagen._tags import Tags from mutagen._tags import Tags
from mutagen._util import DictProxy, convert_error, read_full from mutagen._util import DictProxy, convert_error, read_full
from mutagen._compat import PY3, text_type, itervalues
from ._util import BitPaddedInt, unsynch, ID3JunkFrameError, \ from ._util import BitPaddedInt, unsynch, ID3JunkFrameError, \
ID3EncryptionUnsupportedError, is_valid_frame_id, error, \ ID3EncryptionUnsupportedError, is_valid_frame_id, error, \
@@ -82,10 +83,7 @@ class ID3Header(object):
if self.f_extended: if self.f_extended:
extsize_data = read_full(fileobj, 4) extsize_data = read_full(fileobj, 4)
if PY3:
frame_id = extsize_data.decode("ascii", "replace") frame_id = extsize_data.decode("ascii", "replace")
else:
frame_id = extsize_data
if frame_id in Frames: if frame_id in Frames:
# Some tagger sets the extended header flag but # Some tagger sets the extended header flag but
@@ -131,7 +129,6 @@ def determine_bpi(data, frames, EMPTY=b"\x00" * 10):
name, size, flags = struct.unpack('>4sLH', part) name, size, flags = struct.unpack('>4sLH', part)
size = BitPaddedInt(size) size = BitPaddedInt(size)
o += 10 + size o += 10 + size
if PY3:
try: try:
name = name.decode("ascii") name = name.decode("ascii")
except UnicodeDecodeError: except UnicodeDecodeError:
@@ -151,7 +148,6 @@ def determine_bpi(data, frames, EMPTY=b"\x00" * 10):
break break
name, size, flags = struct.unpack('>4sLH', part) name, size, flags = struct.unpack('>4sLH', part)
o += 10 + size o += 10 + size
if PY3:
try: try:
name = name.decode("ascii") name = name.decode("ascii")
except UnicodeDecodeError: except UnicodeDecodeError:
@@ -191,7 +187,7 @@ class ID3Tags(DictProxy, Tags):
order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"] order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"]
framedata = [ framedata = [
(f, save_frame(f, config=config)) for f in itervalues(self)] (f, save_frame(f, config=config)) for f in self.values()]
def get_prio(frame): def get_prio(frame):
try: try:
@@ -236,14 +232,14 @@ class ID3Tags(DictProxy, Tags):
return [self[key]] return [self[key]]
else: else:
key = key + ":" key = key + ":"
return [v for s, v in list(self.items()) if s.startswith(key)] return [v for s, v in self.items() if s.startswith(key)]
def setall(self, key, values): def setall(self, key, values):
"""Delete frames of the given type and add frames in 'values'. """Delete frames of the given type and add frames in 'values'.
Args: Args:
key (text): key for frames to delete key (text): key for frames to delete
values (List[`Frame`]): frames to add values (list[Frame]): frames to add
""" """
self.delall(key) self.delall(key)
@@ -280,7 +276,7 @@ class ID3Tags(DictProxy, Tags):
``POPM=user@example.org=3 128/255`` ``POPM=user@example.org=3 128/255``
""" """
frames = sorted(Frame.pprint(s) for s in list(self.values())) frames = sorted(Frame.pprint(s) for s in self.values())
return "\n".join(frames) return "\n".join(frames)
def _add(self, frame, strict): def _add(self, frame, strict):
@@ -369,24 +365,23 @@ class ID3Tags(DictProxy, Tags):
self.__update_common() self.__update_common()
# TDAT, TYER, and TIME have been turned into TDRC. # TDAT, TYER, and TIME have been turned into TDRC.
try: timestamps = []
date = text_type(self.get("TYER", "")) old_frames = [self.pop(n, []) for n in ["TYER", "TDAT", "TIME"]]
if date.strip("\x00"): for y, d, t in zip_longest(*old_frames, fillvalue=u""):
self.pop("TYER") ym = re.match(r"([0-9]+)\Z", y)
dat = text_type(self.get("TDAT", "")) dm = re.match(r"([0-9]{2})([0-9]{2})\Z", d)
if dat.strip("\x00"): tm = re.match(r"([0-9]{2})([0-9]{2})\Z", t)
self.pop("TDAT") timestamp = ""
date = "%s-%s-%s" % (date, dat[2:], dat[:2]) if ym:
time = text_type(self.get("TIME", "")) timestamp += u"%s" % ym.groups()
if time.strip("\x00"): if dm:
self.pop("TIME") timestamp += u"-%s-%s" % dm.groups()[::-1]
date += "T%s:%s:00" % (time[:2], time[2:]) if tm:
if "TDRC" not in self: timestamp += u"T%s:%s:00" % tm.groups()
self.add(TDRC(encoding=0, text=date)) if timestamp:
except UnicodeDecodeError: timestamps.append(timestamp)
# Old ID3 tags have *lots* of Unicode problems, so if TYER if timestamps and "TDRC" not in self:
# is bad, just chuck the frames. self.add(TDRC(encoding=0, text=timestamps))
pass
# TORY can be the first part of a TDOR. # TORY can be the first part of a TDOR.
if "TORY" in self: if "TORY" in self:
@@ -482,7 +477,7 @@ class ID3Tags(DictProxy, Tags):
def _copy(self): def _copy(self):
"""Creates a shallow copy of all tags""" """Creates a shallow copy of all tags"""
items = list(self.items()) items = self.items()
subs = {} subs = {}
for f in (self.getall("CHAP") + self.getall("CTOC")): for f in (self.getall("CHAP") + self.getall("CTOC")):
subs[f.HashKey] = f.sub_frames._copy() subs[f.HashKey] = f.sub_frames._copy()
@@ -533,7 +528,6 @@ def save_frame(frame, name=None, config=None):
frame_name = name frame_name = name
else: else:
frame_name = type(frame).__name__ frame_name = type(frame).__name__
if PY3:
frame_name = frame_name.encode("ascii") frame_name = frame_name.encode("ascii")
header = struct.pack('>4s4sH', frame_name, datasize, flags) header = struct.pack('>4s4sH', frame_name, datasize, flags)
@@ -575,7 +569,6 @@ def read_frames(id3, data, frames):
if size == 0: if size == 0:
continue # drop empty frames continue # drop empty frames
if PY3:
try: try:
name = name.decode('ascii') name = name.decode('ascii')
except UnicodeDecodeError: except UnicodeDecodeError:
@@ -614,7 +607,6 @@ def read_frames(id3, data, frames):
if size == 0: if size == 0:
continue # drop empty frames continue # drop empty frames
if PY3:
try: try:
name = name.decode('ascii') name = name.decode('ascii')
except UnicodeDecodeError: except UnicodeDecodeError:
Executable → Regular
+2 -12
View File
@@ -8,7 +8,6 @@
# the Free Software Foundation; either version 2 of the License, or # the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version. # (at your option) any later version.
from mutagen._compat import long_, integer_types, PY3
from mutagen._util import MutagenError from mutagen._util import MutagenError
@@ -110,7 +109,7 @@ class _BitPaddedMixin(object):
mask = (((1 << (8 - bits)) - 1) << bits) mask = (((1 << (8 - bits)) - 1) << bits)
if isinstance(value, integer_types): if isinstance(value, int):
while value: while value:
if value & mask: if value & mask:
return False return False
@@ -133,7 +132,7 @@ class BitPaddedInt(int, _BitPaddedMixin):
numeric_value = 0 numeric_value = 0
shift = 0 shift = 0
if isinstance(value, integer_types): if isinstance(value, int):
if value < 0: if value < 0:
raise ValueError raise ValueError
while value: while value:
@@ -149,21 +148,12 @@ class BitPaddedInt(int, _BitPaddedMixin):
else: else:
raise TypeError raise TypeError
if isinstance(numeric_value, int):
self = int.__new__(BitPaddedInt, numeric_value) self = int.__new__(BitPaddedInt, numeric_value)
else:
self = long_.__new__(BitPaddedLong, numeric_value)
self.bits = bits self.bits = bits
self.bigendian = bigendian self.bigendian = bigendian
return self return self
if PY3:
BitPaddedLong = BitPaddedInt
else:
class BitPaddedLong(long_, _BitPaddedMixin):
pass
class ID3BadUnsynchData(error, ValueError): class ID3BadUnsynchData(error, ValueError):
"""Deprecated""" """Deprecated"""
Executable → Regular
+2 -2
View File
@@ -66,7 +66,7 @@ class M4ATags(DictProxy, Tags):
raise error("deprecated") raise error("deprecated")
def pprint(self): def pprint(self):
return "" return u""
class M4AInfo(StreamInfo): class M4AInfo(StreamInfo):
@@ -77,7 +77,7 @@ class M4AInfo(StreamInfo):
raise error("deprecated") raise error("deprecated")
def pprint(self): def pprint(self):
return "" return u""
class M4A(FileType): class M4A(FileType):
Executable → Regular
+5 -3
View File
@@ -18,10 +18,9 @@ __all__ = ["MonkeysAudio", "Open", "delete"]
import struct import struct
from ._compat import endswith
from mutagen import StreamInfo from mutagen import StreamInfo
from mutagen.apev2 import APEv2File, error, delete from mutagen.apev2 import APEv2File, error, delete
from mutagen._util import cdata, convert_error from mutagen._util import cdata, convert_error, endswith
class MonkeysAudioHeaderError(error): class MonkeysAudioHeaderError(error):
@@ -66,6 +65,9 @@ class MonkeysAudioInfo(StreamInfo):
blocks_per_frame = 73728 blocks_per_frame = 73728
else: else:
blocks_per_frame = 9216 blocks_per_frame = 9216
self.bits_per_sample = 0
if header[48:].startswith(b"WAVEfmt"):
self.bits_per_sample = struct.unpack("<H", header[74:76])[0]
self.version /= 1000.0 self.version /= 1000.0
self.length = 0.0 self.length = 0.0
if (self.sample_rate != 0) and (total_frames > 0): if (self.sample_rate != 0) and (total_frames > 0):
@@ -74,7 +76,7 @@ class MonkeysAudioInfo(StreamInfo):
self.length = float(total_blocks) / self.sample_rate self.length = float(total_blocks) / self.sample_rate
def pprint(self): def pprint(self):
return "Monkey's Audio %.2f, %.2f seconds, %d Hz" % ( return u"Monkey's Audio %.2f, %.2f seconds, %d Hz" % (
self.version, self.length, self.sample_rate) self.version, self.length, self.sample_rate)
Executable → Regular
+23 -17
View File
@@ -12,8 +12,7 @@ import struct
from mutagen import StreamInfo from mutagen import StreamInfo
from mutagen._util import MutagenError, enum, BitReader, BitReaderError, \ from mutagen._util import MutagenError, enum, BitReader, BitReaderError, \
convert_error convert_error, intround, endswith
from mutagen._compat import endswith, xrange
from mutagen.id3 import ID3FileType, delete from mutagen.id3 import ID3FileType, delete
from mutagen.id3._util import BitPaddedInt from mutagen.id3._util import BitPaddedInt
@@ -165,11 +164,13 @@ class MPEGFrame(object):
# Try to find/parse the Xing header, which trumps the above length # Try to find/parse the Xing header, which trumps the above length
# and bitrate calculation. # and bitrate calculation.
if self.layer == 3: if self.layer == 3:
self._parse_vbr_header(fileobj, self.frame_offset, frame_size) self._parse_vbr_header(fileobj, self.frame_offset, frame_size,
frame_length)
fileobj.seek(self.frame_offset + frame_length, 0) fileobj.seek(self.frame_offset + frame_length, 0)
def _parse_vbr_header(self, fileobj, frame_offset, frame_size): def _parse_vbr_header(self, fileobj, frame_offset, frame_size,
frame_length):
"""Does not raise""" """Does not raise"""
# Xing # Xing
@@ -186,6 +187,12 @@ class MPEGFrame(object):
self.encoder_settings = xing.get_encoder_settings() self.encoder_settings = xing.get_encoder_settings()
if xing.frames != -1: if xing.frames != -1:
samples = frame_size * xing.frames samples = frame_size * xing.frames
if xing.bytes != -1 and samples > 0:
# the first frame is only included in xing.bytes but
# not in xing.frames, skip it.
audio_bytes = max(0, xing.bytes - frame_length)
self.bitrate = intround((
audio_bytes * 8 * self.sample_rate) / float(samples))
if lame is not None: if lame is not None:
samples -= lame.encoder_delay_start samples -= lame.encoder_delay_start
samples -= lame.encoder_padding_end samples -= lame.encoder_padding_end
@@ -194,10 +201,8 @@ class MPEGFrame(object):
# files with low bitrate # files with low bitrate
samples = 0 samples = 0
self.length = float(samples) / self.sample_rate self.length = float(samples) / self.sample_rate
if xing.bytes != -1 and self.length:
self.bitrate = int((xing.bytes * 8) / self.length)
if xing.lame_version_desc: if xing.lame_version_desc:
self.encoder_info = "LAME %s" % xing.lame_version_desc self.encoder_info = u"LAME %s" % xing.lame_version_desc
if lame is not None: if lame is not None:
self.track_gain = lame.track_gain_adjustment self.track_gain = lame.track_gain_adjustment
self.track_peak = lame.track_peak self.track_peak = lame.track_peak
@@ -213,7 +218,7 @@ class MPEGFrame(object):
pass pass
else: else:
self.bitrate_mode = BitrateMode.VBR self.bitrate_mode = BitrateMode.VBR
self.encoder_info = "FhG" self.encoder_info = u"FhG"
self.sketchy = False self.sketchy = False
self.length = float(frame_size * vbri.frames) / self.sample_rate self.length = float(frame_size * vbri.frames) / self.sample_rate
if self.length: if self.length:
@@ -297,8 +302,10 @@ class MPEGInfo(StreamInfo):
Attributes: Attributes:
length (`float`): audio length, in seconds length (`float`): audio length, in seconds
channels (`int`): number of audio channels channels (`int`): number of audio channels
bitrate (`int`): audio bitrate, in bits per second bitrate (`int`): audio bitrate, in bits per second.
sample_rate (`int`) audio sample rate, in Hz In case :attr:`bitrate_mode` is :attr:`BitrateMode.UNKNOWN` the
bitrate is guessed based on the first frame.
sample_rate (`int`): audio sample rate, in Hz
encoder_info (`mutagen.text`): a string containing encoder name and encoder_info (`mutagen.text`): a string containing encoder name and
possibly version. In case a lame tag is present this will start possibly version. In case a lame tag is present this will start
with ``"LAME "``, if unknown it is empty, otherwise the with ``"LAME "``, if unknown it is empty, otherwise the
@@ -318,13 +325,12 @@ class MPEGInfo(StreamInfo):
layer (`int`): 1, 2, or 3 layer (`int`): 1, 2, or 3
mode (`int`): One of STEREO, JOINTSTEREO, DUALCHANNEL, or MONO (0-3) mode (`int`): One of STEREO, JOINTSTEREO, DUALCHANNEL, or MONO (0-3)
protected (`bool`): whether or not the file is "protected" protected (`bool`): whether or not the file is "protected"
padding (`bool`) whether or not audio frames are padded
sketchy (`bool`): if true, the file may not be valid MPEG audio sketchy (`bool`): if true, the file may not be valid MPEG audio
""" """
sketchy = False sketchy = False
encoder_info = "" encoder_info = u""
encoder_settings = "" encoder_settings = u""
bitrate_mode = BitrateMode.UNKNOWN bitrate_mode = BitrateMode.UNKNOWN
track_gain = track_peak = album_gain = album_peak = None track_gain = track_peak = album_gain = album_peak = None
@@ -350,7 +356,7 @@ class MPEGInfo(StreamInfo):
# find a sync in the first 1024K, give up after some invalid syncs # find a sync in the first 1024K, give up after some invalid syncs
max_read = 1024 * 1024 max_read = 1024 * 1024
max_syncs = 1000 max_syncs = 1500
enough_frames = 4 enough_frames = 4
min_frames = 2 min_frames = 2
@@ -411,16 +417,16 @@ class MPEGInfo(StreamInfo):
def pprint(self): def pprint(self):
info = str(self.bitrate_mode).split(".", 1)[-1] info = str(self.bitrate_mode).split(".", 1)[-1]
if self.bitrate_mode == BitrateMode.UNKNOWN: if self.bitrate_mode == BitrateMode.UNKNOWN:
info = "CBR?" info = u"CBR?"
if self.encoder_info: if self.encoder_info:
info += ", %s" % self.encoder_info info += ", %s" % self.encoder_info
if self.encoder_settings: if self.encoder_settings:
info += ", %s" % self.encoder_settings info += ", %s" % self.encoder_settings
s = "MPEG %s layer %d, %d bps (%s), %s Hz, %d chn, %.2f seconds" % ( s = u"MPEG %s layer %d, %d bps (%s), %s Hz, %d chn, %.2f seconds" % (
self.version, self.layer, self.bitrate, info, self.version, self.layer, self.bitrate, info,
self.sample_rate, self.channels, self.length) self.sample_rate, self.channels, self.length)
if self.sketchy: if self.sketchy:
s += " (sketchy)" s += u" (sketchy)"
return s return s
Executable → Regular
+48 -46
View File
@@ -11,10 +11,11 @@ http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
http://wiki.hydrogenaud.io/index.php?title=MP3 http://wiki.hydrogenaud.io/index.php?title=MP3
""" """
from __future__ import division
from functools import partial from functools import partial
from io import BytesIO
from mutagen._util import cdata, BitReader from mutagen._util import cdata, BitReader, iterbytes
from mutagen._compat import xrange, iterbytes, cBytesIO
class LAMEError(Exception): class LAMEError(Exception):
@@ -37,7 +38,9 @@ class LAMEHeader(object):
"""VBR quality: 0..9""" """VBR quality: 0..9"""
track_peak = None track_peak = None
"""Peak signal amplitude as float. None if unknown.""" """Peak signal amplitude as float. 1.0 is maximal signal amplitude
in decoded format. None if unknown.
"""
track_gain_origin = 0 track_gain_origin = 0
"""see the docs""" """see the docs"""
@@ -106,7 +109,7 @@ class LAMEHeader(object):
raise LAMEError("Not enough data") raise LAMEError("Not enough data")
# extended lame header # extended lame header
r = BitReader(cBytesIO(payload)) r = BitReader(BytesIO(payload))
revision = r.bits(4) revision = r.bits(4)
if revision != 0: if revision != 0:
raise LAMEError("unsupported header revision %d" % revision) raise LAMEError("unsupported header revision %d" % revision)
@@ -123,8 +126,7 @@ class LAMEHeader(object):
self.track_peak = None self.track_peak = None
else: else:
# see PutLameVBR() in LAME's VbrTag.c # see PutLameVBR() in LAME's VbrTag.c
self.track_peak = ( self.track_peak = cdata.uint32_be(track_peak_data) / 2 ** 23
cdata.uint32_be(track_peak_data) - 0.5) / 2 ** 23
track_gain_type = r.bits(3) track_gain_type = r.bits(3)
self.track_gain_origin = r.bits(3) self.track_gain_origin = r.bits(3)
sign = r.bits(1) sign = r.bits(1)
@@ -194,64 +196,64 @@ class LAMEHeader(object):
if self.vbr_method == 2: if self.vbr_method == 2:
if version in ((3, 90), (3, 91), (3, 92)) and self.encoding_flags: if version in ((3, 90), (3, 91), (3, 92)) and self.encoding_flags:
if self.bitrate < 255: if self.bitrate < 255:
return "--alt-preset %d" % self.bitrate return u"--alt-preset %d" % self.bitrate
else: else:
return "--alt-preset %d+" % self.bitrate return u"--alt-preset %d+" % self.bitrate
if self.preset_used != 0: if self.preset_used != 0:
return "--preset %d" % self.preset_used return u"--preset %d" % self.preset_used
elif self.bitrate < 255: elif self.bitrate < 255:
return "--abr %d" % self.bitrate return u"--abr %d" % self.bitrate
else: else:
return "--abr %d+" % self.bitrate return u"--abr %d+" % self.bitrate
elif self.vbr_method == 1: elif self.vbr_method == 1:
if self.preset_used == 0: if self.preset_used == 0:
if self.bitrate < 255: if self.bitrate < 255:
return "-b %d" % self.bitrate return u"-b %d" % self.bitrate
else: else:
return "-b 255+" return u"-b 255+"
elif self.preset_used == 1003: elif self.preset_used == 1003:
return "--preset insane" return u"--preset insane"
return "-b %d" % self.preset_used return u"-b %d" % self.preset_used
elif version in ((3, 90), (3, 91), (3, 92)): elif version in ((3, 90), (3, 91), (3, 92)):
preset_key = (self.vbr_quality, self.quality, self.vbr_method, preset_key = (self.vbr_quality, self.quality, self.vbr_method,
self.lowpass_filter, self.ath_type) self.lowpass_filter, self.ath_type)
if preset_key == (1, 2, 4, 19500, 3): if preset_key == (1, 2, 4, 19500, 3):
return "--preset r3mix" return u"--preset r3mix"
if preset_key == (2, 2, 3, 19000, 4): if preset_key == (2, 2, 3, 19000, 4):
return "--alt-preset standard" return u"--alt-preset standard"
if preset_key == (2, 2, 3, 19500, 2): if preset_key == (2, 2, 3, 19500, 2):
return "--alt-preset extreme" return u"--alt-preset extreme"
if self.vbr_method == 3: if self.vbr_method == 3:
return "-V %s" % self.vbr_quality return u"-V %s" % self.vbr_quality
elif self.vbr_method in (4, 5): elif self.vbr_method in (4, 5):
return "-V %s --vbr-new" % self.vbr_quality return u"-V %s --vbr-new" % self.vbr_quality
elif version in ((3, 93), (3, 94), (3, 95), (3, 96), (3, 97)): elif version in ((3, 93), (3, 94), (3, 95), (3, 96), (3, 97)):
if self.preset_used == 1001: if self.preset_used == 1001:
return "--preset standard" return u"--preset standard"
elif self.preset_used == 1002: elif self.preset_used == 1002:
return "--preset extreme" return u"--preset extreme"
elif self.preset_used == 1004: elif self.preset_used == 1004:
return "--preset fast standard" return u"--preset fast standard"
elif self.preset_used == 1005: elif self.preset_used == 1005:
return "--preset fast extreme" return u"--preset fast extreme"
elif self.preset_used == 1006: elif self.preset_used == 1006:
return "--preset medium" return u"--preset medium"
elif self.preset_used == 1007: elif self.preset_used == 1007:
return "--preset fast medium" return u"--preset fast medium"
if self.vbr_method == 3: if self.vbr_method == 3:
return "-V %s" % self.vbr_quality return u"-V %s" % self.vbr_quality
elif self.vbr_method in (4, 5): elif self.vbr_method in (4, 5):
return "-V %s --vbr-new" % self.vbr_quality return u"-V %s --vbr-new" % self.vbr_quality
elif version == (3, 98): elif version == (3, 98):
if self.vbr_method == 3: if self.vbr_method == 3:
return "-V %s --vbr-old" % self.vbr_quality return u"-V %s --vbr-old" % self.vbr_quality
elif self.vbr_method in (4, 5): elif self.vbr_method in (4, 5):
return "-V %s" % self.vbr_quality return u"-V %s" % self.vbr_quality
elif version >= (3, 99): elif version >= (3, 99):
if self.vbr_method == 3: if self.vbr_method == 3:
return "-V %s --vbr-old" % self.vbr_quality return u"-V %s --vbr-old" % self.vbr_quality
elif self.vbr_method in (4, 5): elif self.vbr_method in (4, 5):
p = self.vbr_quality p = self.vbr_quality
adjust_key = (p, self.bitrate, self.lowpass_filter) adjust_key = (p, self.bitrate, self.lowpass_filter)
@@ -261,9 +263,9 @@ class LAMEHeader(object):
(5, 8, 0): 8, (5, 8, 0): 8,
(6, 8, 0): 9, (6, 8, 0): 9,
}.get(adjust_key, p) }.get(adjust_key, p)
return "-V %s" % p return u"-V %s" % p
return "" return u""
@classmethod @classmethod
def parse_version(cls, fileobj): def parse_version(cls, fileobj):
@@ -303,36 +305,36 @@ class LAMEHeader(object):
if (major, minor) < (3, 90) or ( if (major, minor) < (3, 90) or (
(major, minor) == (3, 90) and data[-11:-10] == b"("): (major, minor) == (3, 90) and data[-11:-10] == b"("):
flag = data.strip(b"\x00").rstrip().decode("ascii") flag = data.strip(b"\x00").rstrip().decode("ascii")
return (major, minor), "%d.%d%s" % (major, minor, flag), False return (major, minor), u"%d.%d%s" % (major, minor, flag), False
if len(data) < 11: if len(data) < 11:
raise LAMEError("Invalid version: too long") raise LAMEError("Invalid version: too long")
flag = data[:-11].rstrip(b"\x00") flag = data[:-11].rstrip(b"\x00")
flag_string = "" flag_string = u""
patch = "" patch = u""
if flag == b"a": if flag == b"a":
flag_string = " (alpha)" flag_string = u" (alpha)"
elif flag == b"b": elif flag == b"b":
flag_string = " (beta)" flag_string = u" (beta)"
elif flag == b"r": elif flag == b"r":
patch = ".1+" patch = u".1+"
elif flag == b" ": elif flag == b" ":
if (major, minor) > (3, 96): if (major, minor) > (3, 96):
patch = ".0" patch = u".0"
else: else:
patch = ".0+" patch = u".0+"
elif flag == b"" or flag == b".": elif flag == b"" or flag == b".":
patch = ".0+" patch = u".0+"
else: else:
flag_string = " (?)" flag_string = u" (?)"
# extended header, seek back to 9 bytes for the caller # extended header, seek back to 9 bytes for the caller
fileobj.seek(-11, 1) fileobj.seek(-11, 1)
return (major, minor), \ return (major, minor), \
"%d.%d%s%s" % (major, minor, patch, flag_string), True u"%d.%d%s%s" % (major, minor, patch, flag_string), True
class XingHeaderError(Exception): class XingHeaderError(Exception):
@@ -369,7 +371,7 @@ class XingHeader(object):
lame_version = (0, 0) lame_version = (0, 0)
"""The LAME version as two element tuple (major, minor)""" """The LAME version as two element tuple (major, minor)"""
lame_version_desc = "" lame_version_desc = u""
"""The version of the LAME encoder e.g. '3.99.0'. Empty if unknown""" """The version of the LAME encoder e.g. '3.99.0'. Empty if unknown"""
is_info = False is_info = False
@@ -425,7 +427,7 @@ class XingHeader(object):
"""Returns the guessed encoder settings""" """Returns the guessed encoder settings"""
if self.lame_header is None: if self.lame_header is None:
return "" return u""
return self.lame_header.guess_settings(*self.lame_version) return self.lame_header.guess_settings(*self.lame_version)
@classmethod @classmethod
Executable → Regular
+195 -34
View File
@@ -25,13 +25,15 @@ were all consulted.
import struct import struct
import sys import sys
from io import BytesIO
from collections.abc import Sequence
from datetime import timedelta
from mutagen import FileType, Tags, StreamInfo, PaddingInfo from mutagen import FileType, Tags, StreamInfo, PaddingInfo
from mutagen._constants import GENRES from mutagen._constants import GENRES
from mutagen._util import cdata, insert_bytes, DictProxy, MutagenError, \ from mutagen._util import cdata, insert_bytes, DictProxy, MutagenError, \
hashable, enum, get_size, resize_bytes, loadfile, convert_error hashable, enum, get_size, resize_bytes, loadfile, convert_error, bchr, \
from mutagen._compat import (reraise, PY2, string_types, text_type, chr_, reraise
iteritems, PY3, cBytesIO, izip, xrange)
from ._atom import Atoms, Atom, AtomError from ._atom import Atoms, Atom, AtomError
from ._util import parse_full_atom from ._util import parse_full_atom
from ._as_entry import AudioSampleEntry, ASEntryError from ._as_entry import AudioSampleEntry, ASEntryError
@@ -205,14 +207,10 @@ class MP4FreeForm(bytes):
def _name2key(name): def _name2key(name):
if PY2:
return name
return name.decode("latin-1") return name.decode("latin-1")
def _key2name(key): def _key2name(key):
if PY2:
return key
return key.encode("latin-1") return key.encode("latin-1")
@@ -290,6 +288,8 @@ class MP4Tags(DictProxy, Tags):
* 'soco' -- composer sort order * 'soco' -- composer sort order
* 'sosn' -- show sort order * 'sosn' -- show sort order
* 'tvsh' -- show name * 'tvsh' -- show name
* '\\xa9wrk' -- work
* '\\xa9mvn' -- movement
Boolean values: Boolean values:
@@ -309,6 +309,7 @@ class MP4Tags(DictProxy, Tags):
* '\\xa9mvi' -- Movement Index * '\\xa9mvi' -- Movement Index
* 'shwm' -- work/movement * 'shwm' -- work/movement
* 'stik' -- Media Kind * 'stik' -- Media Kind
* 'hdvd' -- HD Video
* 'rtng' -- Content Rating * 'rtng' -- Content Rating
* 'tves' -- TV Episode * 'tves' -- TV Episode
* 'tvsn' -- TV Season * 'tvsn' -- TV Season
@@ -390,17 +391,17 @@ class MP4Tags(DictProxy, Tags):
@convert_error(IOError, error) @convert_error(IOError, error)
@loadfile(writable=True) @loadfile(writable=True)
def save(self, filething, padding=None): def save(self, filething=None, padding=None):
values = [] values = []
items = sorted(list(self.items()), key=lambda kv: _item_sort_key(*kv)) items = sorted(self.items(), key=lambda kv: _item_sort_key(*kv))
for key, value in items: for key, value in items:
try: try:
values.append(self._render(key, value)) values.append(self._render(key, value))
except (TypeError, ValueError) as s: except (TypeError, ValueError) as s:
reraise(MP4MetadataValueError, s, sys.exc_info()[2]) reraise(MP4MetadataValueError, s, sys.exc_info()[2])
for key, failed in iteritems(self._failed_atoms): for key, failed in self._failed_atoms.items():
# don't write atoms back if we have added a new one with # don't write atoms back if we have added a new one with
# the same name, this excludes freeform which can have # the same name, this excludes freeform which can have
# multiple atoms with the same key (most parsers seem to be able # multiple atoms with the same key (most parsers seem to be able
@@ -560,6 +561,9 @@ class MP4Tags(DictProxy, Tags):
if len(head) != 12: if len(head) != 12:
raise MP4MetadataError("truncated atom % r" % atom.name) raise MP4MetadataError("truncated atom % r" % atom.name)
length, name = struct.unpack(">I4s", head[:8]) length, name = struct.unpack(">I4s", head[:8])
if length < 1:
raise MP4MetadataError(
"atom %r has a length of zero" % atom.name)
version = ord(head[8:9]) version = ord(head[8:9])
flags = struct.unpack(">I", b"\x00" + head[9:12])[0] flags = struct.unpack(">I", b"\x00" + head[9:12])[0]
if name != b"data": if name != b"data":
@@ -599,7 +603,9 @@ class MP4Tags(DictProxy, Tags):
if atom_name != b"data": if atom_name != b"data":
raise MP4MetadataError( raise MP4MetadataError(
"unexpected atom %r inside %r" % (atom_name, atom.name)) "unexpected atom %r inside %r" % (atom_name, atom.name))
if length < 1:
raise MP4MetadataError(
"atom %r has a length of zero" % atom.name)
version = ord(data[pos + 8:pos + 8 + 1]) version = ord(data[pos + 8:pos + 8 + 1])
flags = struct.unpack(">I", b"\x00" + data[pos + 9:pos + 12])[0] flags = struct.unpack(">I", b"\x00" + data[pos + 9:pos + 12])[0]
value.append(MP4FreeForm(data[pos + 16:pos + length], value.append(MP4FreeForm(data[pos + 16:pos + length],
@@ -714,7 +720,8 @@ class MP4Tags(DictProxy, Tags):
# by itunes for compatibility. # by itunes for compatibility.
if cdata.int8_min <= v <= cdata.int8_max and min_bytes <= 1: if cdata.int8_min <= v <= cdata.int8_max and min_bytes <= 1:
data = cdata.to_int8(v) data = cdata.to_int8(v)
if cdata.int16_min <= v <= cdata.int16_max and min_bytes <= 2: elif cdata.int16_min <= v <= cdata.int16_max and \
min_bytes <= 2:
data = cdata.to_int16_be(v) data = cdata.to_int16_be(v)
elif cdata.int32_min <= v <= cdata.int32_max and \ elif cdata.int32_min <= v <= cdata.int32_max and \
min_bytes <= 4: min_bytes <= 4:
@@ -743,7 +750,7 @@ class MP4Tags(DictProxy, Tags):
def __render_bool(self, key, value): def __render_bool(self, key, value):
return self.__render_data( return self.__render_data(
key, 0, AtomDataType.INTEGER, [chr_(bool(value))]) key, 0, AtomDataType.INTEGER, [bchr(bool(value))])
def __parse_cover(self, atom, data): def __parse_cover(self, atom, data):
values = [] values = []
@@ -757,6 +764,9 @@ class MP4Tags(DictProxy, Tags):
continue continue
raise MP4MetadataError( raise MP4MetadataError(
"unexpected atom %r inside 'covr'" % name) "unexpected atom %r inside 'covr'" % name)
if length < 1:
raise MP4MetadataError(
"atom %r has a length of zero" % atom.name)
if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG): if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG):
# Sometimes AtomDataType.IMPLICIT or simply wrong. # Sometimes AtomDataType.IMPLICIT or simply wrong.
# In all cases it was jpeg, so default to it # In all cases it was jpeg, so default to it
@@ -804,18 +814,14 @@ class MP4Tags(DictProxy, Tags):
self.__add(key, values) self.__add(key, values)
def __render_text(self, key, value, flags=AtomDataType.UTF8): def __render_text(self, key, value, flags=AtomDataType.UTF8):
if isinstance(value, string_types): if isinstance(value, str):
value = [value] value = [value]
encoded = [] encoded = []
for v in value: for v in value:
if not isinstance(v, text_type): if not isinstance(v, str):
if PY3:
raise TypeError("%r not str" % v) raise TypeError("%r not str" % v)
try:
v = v.decode("utf-8")
except (AttributeError, UnicodeDecodeError) as e:
raise TypeError(e)
encoded.append(v.encode("utf-8")) encoded.append(v.encode("utf-8"))
return self.__render_data(key, 0, flags, encoded) return self.__render_data(key, 0, flags, encoded)
@@ -849,6 +855,7 @@ class MP4Tags(DictProxy, Tags):
b"pcst": (__parse_bool, __render_bool), b"pcst": (__parse_bool, __render_bool),
b"shwm": (__parse_integer, __render_integer, 1), b"shwm": (__parse_integer, __render_integer, 1),
b"stik": (__parse_integer, __render_integer, 1), b"stik": (__parse_integer, __render_integer, 1),
b"hdvd": (__parse_integer, __render_integer, 1),
b"rtng": (__parse_integer, __render_integer, 1), b"rtng": (__parse_integer, __render_integer, 1),
b"covr": (__parse_cover, __render_cover), b"covr": (__parse_cover, __render_cover),
b"purl": (__parse_text, __render_text), b"purl": (__parse_text, __render_text),
@@ -866,24 +873,141 @@ class MP4Tags(DictProxy, Tags):
def pprint(self): def pprint(self):
def to_line(key, value): def to_line(key, value):
assert isinstance(key, text_type) assert isinstance(key, str)
if isinstance(value, text_type): if isinstance(value, str):
return "%s=%s" % (key, value) return u"%s=%s" % (key, value)
return "%s=%r" % (key, value) return u"%s=%r" % (key, value)
values = [] values = []
for key, value in sorted(iteritems(self)): for key, value in sorted(self.items()):
if not isinstance(key, text_type): if not isinstance(key, str):
key = key.decode("latin-1") key = key.decode("latin-1")
if key == "covr": if key == "covr":
values.append("%s=%s" % (key, ", ".join( values.append(u"%s=%s" % (key, u", ".join(
["[%d bytes of data]" % len(data) for data in value]))) [u"[%d bytes of data]" % len(data) for data in value])))
elif isinstance(value, list): elif isinstance(value, list):
for v in value: for v in value:
values.append(to_line(key, v)) values.append(to_line(key, v))
else: else:
values.append(to_line(key, value)) values.append(to_line(key, value))
return "\n".join(values) return u"\n".join(values)
class Chapter(object):
"""Chapter()
Chapter information container
"""
def __init__(self, start, title):
self.start = start
self.title = title
class MP4Chapters(Sequence):
"""MP4Chapters()
MPEG-4 Chapter information.
Supports the 'moov.udta.chpl' box.
A sequence of Chapter objects with the following members:
start (`float`): position from the start of the file in seconds
title (`str`): title of the chapter
"""
def __init__(self, *args, **kwargs):
self._timescale = None
self._duration = None
self._chapters = []
super(MP4Chapters, self).__init__()
if args or kwargs:
self.load(*args, **kwargs)
def __len__(self):
return self._chapters.__len__()
def __getitem__(self, key):
return self._chapters.__getitem__(key)
def load(self, atoms, fileobj):
try:
mvhd = atoms.path(b"moov", b"mvhd")[-1]
except KeyError as key:
return MP4MetadataError(key)
self._parse_mvhd(mvhd, fileobj)
if not self._timescale:
raise MP4MetadataError("Unable to get timescale")
try:
chpl = atoms.path(b"moov", b"udta", b"chpl")[-1]
except KeyError as key:
return MP4MetadataError(key)
self._parse_chpl(chpl, fileobj)
@classmethod
def _can_load(cls, atoms):
return b"moov.udta.chpl" in atoms and b"moov.mvhd" in atoms
def _parse_mvhd(self, atom, fileobj):
assert atom.name == b"mvhd"
ok, data = atom.read(fileobj)
if not ok:
raise MP4StreamInfoError("Invalid mvhd")
version = data[0]
pos = 4
if version == 0:
pos += 8 # created, modified
self._timescale = struct.unpack(">l", data[pos:pos + 4])[0]
pos += 4
self._duration = struct.unpack(">l", data[pos:pos + 4])[0]
pos += 4
elif version == 1:
pos += 16 # created, modified
self._timescale = struct.unpack(">l", data[pos:pos + 4])[0]
pos += 4
self._duration = struct.unpack(">q", data[pos:pos + 8])[0]
pos += 8
def _parse_chpl(self, atom, fileobj):
assert atom.name == b"chpl"
ok, data = atom.read(fileobj)
if not ok:
raise MP4StreamInfoError("Invalid atom")
chapters = data[8]
pos = 9
for i in range(chapters):
start = struct.unpack(">Q", data[pos:pos + 8])[0] / 10000
pos += 8
title_len = data[pos]
pos += 1
try:
title = data[pos:pos + title_len].decode()
except UnicodeDecodeError as e:
raise MP4MetadataError("chapter %d title: %s" % (i, e))
pos += title_len
self._chapters.append(Chapter(start / self._timescale, title))
def pprint(self):
chapters = ["%s %s" % (timedelta(seconds=chapter.start), chapter.title)
for chapter in self._chapters]
return "chapters=%s" % '\n '.join(chapters)
class MP4Info(StreamInfo): class MP4Info(StreamInfo):
@@ -915,8 +1039,8 @@ class MP4Info(StreamInfo):
channels = 0 channels = 0
sample_rate = 0 sample_rate = 0
bits_per_sample = 0 bits_per_sample = 0
codec = "" codec = u""
codec_description = "" codec_description = u""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if args or kwargs: if args or kwargs:
@@ -1001,7 +1125,7 @@ class MP4Info(StreamInfo):
return return
# look at the first entry if there is one # look at the first entry if there is one
entry_fileobj = cBytesIO(data[offset:]) entry_fileobj = BytesIO(data[offset:])
try: try:
entry_atom = Atom(entry_fileobj) entry_atom = Atom(entry_fileobj)
except AtomError as e: except AtomError as e:
@@ -1041,6 +1165,7 @@ class MP4(FileType):
""" """
MP4Tags = MP4Tags MP4Tags = MP4Tags
MP4Chapters = MP4Chapters
_mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"] _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"]
@@ -1065,7 +1190,6 @@ class MP4(FileType):
if not MP4Tags._can_load(atoms): if not MP4Tags._can_load(atoms):
self.tags = None self.tags = None
self._padding = 0
else: else:
try: try:
self.tags = self.MP4Tags(atoms, fileobj) self.tags = self.MP4Tags(atoms, fileobj)
@@ -1073,14 +1197,51 @@ class MP4(FileType):
raise raise
except Exception as err: except Exception as err:
reraise(MP4MetadataError, err, sys.exc_info()[2]) reraise(MP4MetadataError, err, sys.exc_info()[2])
if not MP4Chapters._can_load(atoms):
self.chapters = None
else: else:
self._padding = self.tags._padding try:
self.chapters = self.MP4Chapters(atoms, fileobj)
except error:
raise
except Exception as err:
reraise(MP4MetadataError, err, sys.exc_info()[2])
@property
def _padding(self):
if self.tags is None:
return 0
else:
return self.tags._padding
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""save(filething=None, padding=None)""" """save(filething=None, padding=None)"""
super(MP4, self).save(*args, **kwargs) super(MP4, self).save(*args, **kwargs)
def pprint(self):
"""
Returns:
text: stream information, comment key=value pairs and chapters.
"""
stream = "%s (%s)" % (self.info.pprint(), self.mime[0])
try:
tags = self.tags.pprint()
except AttributeError:
pass
else:
stream += ((tags and "\n" + tags) or "")
try:
chapters = self.chapters.pprint()
except AttributeError:
pass
else:
stream += "\n" + chapters
return stream
def add_tags(self): def add_tags(self):
if self.tags is None: if self.tags is None:
self.tags = self.MP4Tags() self.tags = self.MP4Tags()
Executable → Regular
+15 -11
View File
@@ -6,10 +6,10 @@
# the Free Software Foundation; either version 2 of the License, or # the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version. # (at your option) any later version.
from mutagen._compat import cBytesIO, xrange from io import BytesIO
from mutagen.aac import ProgramConfigElement from mutagen.aac import ProgramConfigElement
from mutagen._util import BitReader, BitReaderError, cdata from mutagen._util import BitReader, BitReaderError, cdata
from mutagen._compat import text_type
from ._util import parse_full_atom from ._util import parse_full_atom
from ._atom import Atom, AtomError from ._atom import Atom, AtomError
@@ -47,7 +47,7 @@ class AudioSampleEntry(object):
if not ok: if not ok:
raise ASEntryError("too short %r atom" % atom.name) raise ASEntryError("too short %r atom" % atom.name)
fileobj = cBytesIO(data) fileobj = BytesIO(data)
r = BitReader(fileobj) r = BitReader(fileobj)
try: try:
@@ -93,7 +93,7 @@ class AudioSampleEntry(object):
ok, data = atom.read(fileobj) ok, data = atom.read(fileobj)
if not ok: if not ok:
raise ASEntryError("truncated %s atom" % atom.name) raise ASEntryError("truncated %s atom" % atom.name)
fileobj = cBytesIO(data) fileobj = BytesIO(data)
r = BitReader(fileobj) r = BitReader(fileobj)
# sample_rate in AudioSampleEntry covers values in # sample_rate in AudioSampleEntry covers values in
@@ -134,7 +134,7 @@ class AudioSampleEntry(object):
if version != 0: if version != 0:
raise ASEntryError("Unsupported version %d" % version) raise ASEntryError("Unsupported version %d" % version)
fileobj = cBytesIO(data) fileobj = BytesIO(data)
r = BitReader(fileobj) r = BitReader(fileobj)
try: try:
@@ -168,7 +168,7 @@ class AudioSampleEntry(object):
if version != 0: if version != 0:
raise ASEntryError("Unsupported version %d" % version) raise ASEntryError("Unsupported version %d" % version)
fileobj = cBytesIO(data) fileobj = BytesIO(data)
r = BitReader(fileobj) r = BitReader(fileobj)
try: try:
@@ -239,9 +239,13 @@ class BaseDescriptor(object):
pos = fileobj.tell() pos = fileobj.tell()
instance = cls(fileobj, length) instance = cls(fileobj, length)
left = length - (fileobj.tell() - pos) left = length - (fileobj.tell() - pos)
if left < 0: if left > 0:
raise DescriptorError("descriptor parsing read too much data")
fileobj.seek(left, 1) fileobj.seek(left, 1)
else:
# XXX: In case the instance length is shorted than the content
# assume the size is wrong and just continue parsing
# https://github.com/quodlibet/mutagen/issues/444
pass
return instance return instance
@@ -318,10 +322,10 @@ class DecoderConfigDescriptor(BaseDescriptor):
def codec_param(self): def codec_param(self):
"""string""" """string"""
param = ".%X" % self.objectTypeIndication param = u".%X" % self.objectTypeIndication
info = self.decSpecificInfo info = self.decSpecificInfo
if info is not None: if info is not None:
param += ".%d" % info.audioObjectType param += u".%d" % info.audioObjectType
return param return param
@property @property
@@ -371,7 +375,7 @@ class DecoderSpecificInfo(BaseDescriptor):
name += "+SBR" name += "+SBR"
if self.psPresentFlag == 1: if self.psPresentFlag == 1:
name += "+PS" name += "+PS"
return text_type(name) return str(name)
@property @property
def sample_rate(self): def sample_rate(self):
Executable → Regular
-5
View File
@@ -8,7 +8,6 @@
import struct import struct
from mutagen._compat import PY2
from mutagen._util import convert_error from mutagen._util import convert_error
# This is not an exhaustive list of container atoms, but just the # This is not an exhaustive list of container atoms, but just the
@@ -180,10 +179,6 @@ class Atoms(object):
specifying the complete path ('moov.udta'). specifying the complete path ('moov.udta').
""" """
if PY2:
if isinstance(names, str):
names = names.split(b".")
else:
if isinstance(names, bytes): if isinstance(names, bytes):
names = names.split(b".") names = names.split(b".")
Executable → Regular
View File
Executable → Regular
+15 -8
View File
@@ -19,11 +19,10 @@ __all__ = ["Musepack", "Open", "delete"]
import struct import struct
from ._compat import endswith, xrange
from mutagen import StreamInfo from mutagen import StreamInfo
from mutagen.apev2 import APEv2File, error, delete from mutagen.apev2 import APEv2File, error, delete
from mutagen.id3._util import BitPaddedInt from mutagen.id3._util import BitPaddedInt
from mutagen._util import cdata, convert_error from mutagen._util import cdata, convert_error, intround, endswith
class MusepackHeaderError(error): class MusepackHeaderError(error):
@@ -118,7 +117,7 @@ class MusepackInfo(StreamInfo):
if not self.bitrate and self.length != 0: if not self.bitrate and self.length != 0:
fileobj.seek(0, 2) fileobj.seek(0, 2)
self.bitrate = int(round(fileobj.tell() * 8 / self.length)) self.bitrate = intround(fileobj.tell() * 8 / self.length)
def __parse_sv8(self, fileobj): def __parse_sv8(self, fileobj):
# SV8 http://trac.musepack.net/trac/wiki/SV8Specification # SV8 http://trac.musepack.net/trac/wiki/SV8Specification
@@ -143,9 +142,13 @@ class MusepackInfo(StreamInfo):
# packets can be at maximum data_size big and are padded with zeros # packets can be at maximum data_size big and are padded with zeros
if frame_type == b"SH": if frame_type == b"SH":
if frame_type not in mandatory_packets:
raise MusepackHeaderError("Duplicate SH packet")
mandatory_packets.remove(frame_type) mandatory_packets.remove(frame_type)
self.__parse_stream_header(fileobj, data_size) self.__parse_stream_header(fileobj, data_size)
elif frame_type == b"RG": elif frame_type == b"RG":
if frame_type not in mandatory_packets:
raise MusepackHeaderError("Duplicate RG packet")
mandatory_packets.remove(frame_type) mandatory_packets.remove(frame_type)
self.__parse_replaygain_packet(fileobj, data_size) self.__parse_replaygain_packet(fileobj, data_size)
else: else:
@@ -184,9 +187,13 @@ class MusepackInfo(StreamInfo):
remaining_size -= l1 + l2 remaining_size -= l1 + l2
data = fileobj.read(remaining_size) data = fileobj.read(remaining_size)
if len(data) != remaining_size: if len(data) != remaining_size or len(data) < 2:
raise MusepackHeaderError("SH packet ended unexpectedly.") raise MusepackHeaderError("SH packet ended unexpectedly.")
self.sample_rate = RATES[bytearray(data)[0] >> 5] rate_index = (bytearray(data)[0] >> 5)
try:
self.sample_rate = RATES[rate_index]
except IndexError:
raise MusepackHeaderError("Invalid sample rate")
self.channels = (bytearray(data)[1] >> 4) + 1 self.channels = (bytearray(data)[1] >> 4) + 1
def __parse_replaygain_packet(self, fileobj, data_size): def __parse_replaygain_packet(self, fileobj, data_size):
@@ -253,12 +260,12 @@ class MusepackInfo(StreamInfo):
def pprint(self): def pprint(self):
rg_data = [] rg_data = []
if hasattr(self, "title_gain"): if hasattr(self, "title_gain"):
rg_data.append("%+0.2f (title)" % self.title_gain) rg_data.append(u"%+0.2f (title)" % self.title_gain)
if hasattr(self, "album_gain"): if hasattr(self, "album_gain"):
rg_data.append("%+0.2f (album)" % self.album_gain) rg_data.append(u"%+0.2f (album)" % self.album_gain)
rg_data = (rg_data and ", Gain: " + ", ".join(rg_data)) or "" rg_data = (rg_data and ", Gain: " + ", ".join(rg_data)) or ""
return "Musepack SV%d, %.2f seconds, %d Hz, %d bps%s" % ( return u"Musepack SV%d, %.2f seconds, %d Hz, %d bps%s" % (
self.version, self.length, self.sample_rate, self.bitrate, rg_data) self.version, self.length, self.sample_rate, self.bitrate, rg_data)
Executable → Regular
+30 -18
View File
@@ -19,10 +19,11 @@ http://www.xiph.org/ogg/doc/rfc3533.txt.
import struct import struct
import sys import sys
import zlib import zlib
from io import BytesIO
from mutagen import FileType from mutagen import FileType
from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, seek_end from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, \
from ._compat import cBytesIO, reraise, chr_, izip, xrange seek_end, bchr, reraise
class error(MutagenError): class error(MutagenError):
@@ -37,7 +38,7 @@ class OggPage(object):
A page is a header of 26 bytes, followed by the length of the A page is a header of 26 bytes, followed by the length of the
data, followed by the data. data, followed by the data.
The constructor is givin a file-like object pointing to the start The constructor is given a file-like object pointing to the start
of an Ogg page. After the constructor is finished it is pointing of an Ogg page. After the constructor is finished it is pointing
to the start of the next page. to the start of the next page.
@@ -50,7 +51,7 @@ class OggPage(object):
offset (`int` or `None`): offset this page was read from (default None) offset (`int` or `None`): offset this page was read from (default None)
complete (`bool`): if the last packet on this page is complete complete (`bool`): if the last packet on this page is complete
(default True) (default True)
packets (List[`bytes`]): list of raw packet data (default []) packets (list[bytes]): list of raw packet data (default [])
Note that if 'complete' is false, the next page's 'continued' Note that if 'complete' is false, the next page's 'continued'
property must be true (so set both when constructing pages). property must be true (so set both when constructing pages).
@@ -145,11 +146,11 @@ class OggPage(object):
lacing_data = [] lacing_data = []
for datum in self.packets: for datum in self.packets:
quot, rem = divmod(len(datum), 255) quot, rem = divmod(len(datum), 255)
lacing_data.append(b"\xff" * quot + chr_(rem)) lacing_data.append(b"\xff" * quot + bchr(rem))
lacing_data = b"".join(lacing_data) lacing_data = b"".join(lacing_data)
if not self.complete and lacing_data.endswith(b"\x00"): if not self.complete and lacing_data.endswith(b"\x00"):
lacing_data = lacing_data[:-1] lacing_data = lacing_data[:-1]
data.append(chr_(len(lacing_data))) data.append(bchr(len(lacing_data)))
data.append(lacing_data) data.append(lacing_data)
data.extend(self.packets) data.extend(self.packets)
data = b"".join(data) data = b"".join(data)
@@ -216,7 +217,7 @@ class OggPage(object):
so also the CRC. so also the CRC.
If an error occurs (e.g. non-Ogg data is found), fileobj will If an error occurs (e.g. non-Ogg data is found), fileobj will
be left pointing to the place in the stream the error occured, be left pointing to the place in the stream the error occurred,
but the invalid data will be left intact (since this function but the invalid data will be left intact (since this function
does not change the total file size). does not change the total file size).
""" """
@@ -267,6 +268,7 @@ class OggPage(object):
else: else:
sequence += 1 sequence += 1
if page.packets:
if page.continued: if page.continued:
packets[-1].append(page.packets[0]) packets[-1].append(page.packets[0])
else: else:
@@ -434,7 +436,7 @@ class OggPage(object):
cls.renumber(fileobj, serial, sequence) cls.renumber(fileobj, serial, sequence)
@staticmethod @staticmethod
def find_last(fileobj, serial): def find_last(fileobj, serial, finishing=False):
"""Find the last page of the stream 'serial'. """Find the last page of the stream 'serial'.
If the file is not multiplexed this function is fast. If it is, If the file is not multiplexed this function is fast. If it is,
@@ -443,6 +445,10 @@ class OggPage(object):
This finds the last page in the actual file object, or the last This finds the last page in the actual file object, or the last
page in the stream (with eos set), whichever comes first. page in the stream (with eos set), whichever comes first.
If finishing is True it returns the last page which contains a packet
finishing on it. If there exist pages but none with finishing packets
returns None.
Returns None in case no page with the serial exists. Returns None in case no page with the serial exists.
Raises error in case this isn't a valid ogg stream. Raises error in case this isn't a valid ogg stream.
Raises IOError. Raises IOError.
@@ -456,14 +462,18 @@ class OggPage(object):
index = data.rindex(b"OggS") index = data.rindex(b"OggS")
except ValueError: except ValueError:
raise error("unable to find final Ogg header") raise error("unable to find final Ogg header")
bytesobj = cBytesIO(data[index:]) bytesobj = BytesIO(data[index:])
def is_valid(page):
return not finishing or page.position != -1
best_page = None best_page = None
try: try:
page = OggPage(bytesobj) page = OggPage(bytesobj)
except error: except error:
pass pass
else: else:
if page.serial == serial: if page.serial == serial and is_valid(page):
if page.last: if page.last:
return page return page
else: else:
@@ -475,12 +485,14 @@ class OggPage(object):
fileobj.seek(0) fileobj.seek(0)
try: try:
page = OggPage(fileobj) page = OggPage(fileobj)
while not page.last: while True:
page = OggPage(fileobj) if page.serial == serial:
while page.serial != serial: if is_valid(page):
page = OggPage(fileobj)
best_page = page best_page = page
return page if page.last:
break
page = OggPage(fileobj)
return best_page
except error: except error:
return best_page return best_page
except EOFError: except EOFError:
@@ -525,7 +537,7 @@ class OggFileType(FileType):
raise self._Error("no appropriate stream found") raise self._Error("no appropriate stream found")
@loadfile(writable=True) @loadfile(writable=True)
def delete(self, filething): def delete(self, filething=None):
"""delete(filething=None) """delete(filething=None)
Remove tags from a file. Remove tags from a file.
@@ -557,7 +569,7 @@ class OggFileType(FileType):
raise self._Error raise self._Error
@loadfile(writable=True) @loadfile(writable=True)
def save(self, filething, padding=None): def save(self, filething=None, padding=None):
"""save(filething=None, padding=None) """save(filething=None, padding=None)
Save a tag to a file. Save a tag to a file.
@@ -566,7 +578,7 @@ class OggFileType(FileType):
Args: Args:
filething (filething) filething (filething)
padding (PaddingFunction) padding (:obj:`mutagen.PaddingFunction`)
Raises: Raises:
mutagen.MutagenError mutagen.MutagenError
""" """
Executable → Regular
+7 -6
View File
@@ -18,8 +18,7 @@ http://flac.sourceforge.net/ogg_mapping.html.
__all__ = ["OggFLAC", "Open", "delete"] __all__ = ["OggFLAC", "Open", "delete"]
import struct import struct
from io import BytesIO
from ._compat import cBytesIO
from mutagen import StreamInfo from mutagen import StreamInfo
from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError
@@ -65,7 +64,7 @@ class OggFLACStreamInfo(StreamInfo):
self.serial = page.serial self.serial = page.serial
# Skip over the block header. # Skip over the block header.
stringobj = cBytesIO(page.packets[0][17:]) stringobj = BytesIO(page.packets[0][17:])
try: try:
flac_info = FLACStreamInfo(stringobj) flac_info = FLACStreamInfo(stringobj)
@@ -79,11 +78,13 @@ class OggFLACStreamInfo(StreamInfo):
def _post_tags(self, fileobj): def _post_tags(self, fileobj):
if self.length: if self.length:
return return
page = OggPage.find_last(fileobj, self.serial) page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None:
raise OggFLACHeaderError
self.length = page.position / float(self.sample_rate) self.length = page.position / float(self.sample_rate)
def pprint(self): def pprint(self):
return "Ogg FLAC, %.2f seconds, %d Hz" % ( return u"Ogg FLAC, %.2f seconds, %d Hz" % (
self.length, self.sample_rate) self.length, self.sample_rate)
@@ -99,7 +100,7 @@ class OggFLACVComment(VCommentDict):
if page.serial == info.serial: if page.serial == info.serial:
pages.append(page) pages.append(page)
complete = page.complete or (len(page.packets) > 1) complete = page.complete or (len(page.packets) > 1)
comment = cBytesIO(OggPage.to_packets(pages)[0][4:]) comment = BytesIO(OggPage.to_packets(pages)[0][4:])
super(OggFLACVComment, self).__init__(comment, framing=False) super(OggFLACVComment, self).__init__(comment, framing=False)
def _inject(self, fileobj, padding_func): def _inject(self, fileobj, padding_func):
Executable → Regular
+3 -3
View File
@@ -17,9 +17,9 @@ Based on http://tools.ietf.org/html/draft-terriberry-oggopus-01
__all__ = ["OggOpus", "Open", "delete"] __all__ = ["OggOpus", "Open", "delete"]
import struct import struct
from io import BytesIO
from mutagen import StreamInfo from mutagen import StreamInfo
from mutagen._compat import BytesIO
from mutagen._util import get_size, loadfile, convert_error from mutagen._util import get_size, loadfile, convert_error
from mutagen._tags import PaddingInfo from mutagen._tags import PaddingInfo
from mutagen._vorbis import VCommentDict from mutagen._vorbis import VCommentDict
@@ -69,13 +69,13 @@ class OggOpusInfo(StreamInfo):
raise OggOpusHeaderError("version %r unsupported" % major) raise OggOpusHeaderError("version %r unsupported" % major)
def _post_tags(self, fileobj): def _post_tags(self, fileobj):
page = OggPage.find_last(fileobj, self.serial) page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None: if page is None:
raise OggOpusHeaderError raise OggOpusHeaderError
self.length = (page.position - self.__pre_skip) / float(48000) self.length = (page.position - self.__pre_skip) / float(48000)
def pprint(self): def pprint(self):
return "Ogg Opus, %.2f seconds" % (self.length) return u"Ogg Opus, %.2f seconds" % (self.length)
class OggOpusVComment(VCommentDict): class OggOpusVComment(VCommentDict):
Executable → Regular
+2 -2
View File
@@ -64,13 +64,13 @@ class OggSpeexInfo(StreamInfo):
self.serial = page.serial self.serial = page.serial
def _post_tags(self, fileobj): def _post_tags(self, fileobj):
page = OggPage.find_last(fileobj, self.serial) page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None: if page is None:
raise OggSpeexHeaderError raise OggSpeexHeaderError
self.length = page.position / float(self.sample_rate) self.length = page.position / float(self.sample_rate)
def pprint(self): def pprint(self):
return "Ogg Speex, %.2f seconds" % self.length return u"Ogg Speex, %.2f seconds" % self.length
class OggSpeexVComment(VCommentDict): class OggSpeexVComment(VCommentDict):
Executable → Regular
+15 -5
View File
@@ -50,33 +50,39 @@ class OggTheoraInfo(StreamInfo):
def __init__(self, fileobj): def __init__(self, fileobj):
page = OggPage(fileobj) page = OggPage(fileobj)
while not page.packets[0].startswith(b"\x80theora"): while not page.packets or \
not page.packets[0].startswith(b"\x80theora"):
page = OggPage(fileobj) page = OggPage(fileobj)
if not page.first: if not page.first:
raise OggTheoraHeaderError( raise OggTheoraHeaderError(
"page has ID header, but doesn't start a stream") "page has ID header, but doesn't start a stream")
data = page.packets[0] data = page.packets[0]
if len(data) < 42:
raise OggTheoraHeaderError("Truncated header")
vmaj, vmin = struct.unpack("2B", data[7:9]) vmaj, vmin = struct.unpack("2B", data[7:9])
if (vmaj, vmin) != (3, 2): if (vmaj, vmin) != (3, 2):
raise OggTheoraHeaderError( raise OggTheoraHeaderError(
"found Theora version %d.%d != 3.2" % (vmaj, vmin)) "found Theora version %d.%d != 3.2" % (vmaj, vmin))
fps_num, fps_den = struct.unpack(">2I", data[22:30]) fps_num, fps_den = struct.unpack(">2I", data[22:30])
if not fps_den or not fps_num:
raise OggTheoraHeaderError("FRN or FRD is equal to zero")
self.fps = fps_num / float(fps_den) self.fps = fps_num / float(fps_den)
self.bitrate = cdata.uint_be(b"\x00" + data[37:40]) self.bitrate = cdata.uint_be(b"\x00" + data[37:40])
self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F
self.serial = page.serial self.serial = page.serial
def _post_tags(self, fileobj): def _post_tags(self, fileobj):
page = OggPage.find_last(fileobj, self.serial) page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None: if page is None:
raise OggTheoraHeaderError raise OggTheoraHeaderError
position = page.position position = page.position
mask = (1 << self.granule_shift) - 1 mask = (1 << self.granule_shift) - 1
frames = (position >> self.granule_shift) + (position & mask) frames = (position >> self.granule_shift) + (position & mask)
assert self.fps
self.length = frames / float(self.fps) self.length = frames / float(self.fps)
def pprint(self): def pprint(self):
return "Ogg Theora, %.2f seconds, %d bps" % (self.length, return u"Ogg Theora, %.2f seconds, %d bps" % (self.length,
self.bitrate) self.bitrate)
@@ -91,7 +97,10 @@ class OggTheoraCommentDict(VCommentDict):
if page.serial == info.serial: if page.serial == info.serial:
pages.append(page) pages.append(page)
complete = page.complete or (len(page.packets) > 1) complete = page.complete or (len(page.packets) > 1)
data = OggPage.to_packets(pages)[0][7:] packets = OggPage.to_packets(pages)
if not packets:
raise error("Missing metadata packet")
data = packets[0][7:]
super(OggTheoraCommentDict, self).__init__(data, framing=False) super(OggTheoraCommentDict, self).__init__(data, framing=False)
self._padding = len(data) - self._size self._padding = len(data) - self._size
@@ -100,7 +109,8 @@ class OggTheoraCommentDict(VCommentDict):
fileobj.seek(0) fileobj.seek(0)
page = OggPage(fileobj) page = OggPage(fileobj)
while not page.packets[0].startswith(b"\x81theora"): while not page.packets or \
not page.packets[0].startswith(b"\x81theora"):
page = OggPage(fileobj) page = OggPage(fileobj)
old_pages = [page] old_pages = [page]
Executable → Regular
+11 -4
View File
@@ -43,7 +43,7 @@ class OggVorbisInfo(StreamInfo):
length (`float`): File length in seconds, as a float length (`float`): File length in seconds, as a float
channels (`int`): Number of channels channels (`int`): Number of channels
bitrate (`int`): Nominal ('average') bitrate in bits per second bitrate (`int`): Nominal ('average') bitrate in bits per second
sample_Rate (`int`): Sample rate in Hz sample_rate (`int`): Sample rate in Hz
""" """
@@ -56,13 +56,20 @@ class OggVorbisInfo(StreamInfo):
"""Raises ogg.error, IOError""" """Raises ogg.error, IOError"""
page = OggPage(fileobj) page = OggPage(fileobj)
if not page.packets:
raise OggVorbisHeaderError("page has not packets")
while not page.packets[0].startswith(b"\x01vorbis"): while not page.packets[0].startswith(b"\x01vorbis"):
page = OggPage(fileobj) page = OggPage(fileobj)
if not page.first: if not page.first:
raise OggVorbisHeaderError( raise OggVorbisHeaderError(
"page has ID header, but doesn't start a stream") "page has ID header, but doesn't start a stream")
if len(page.packets[0]) < 28:
raise OggVorbisHeaderError(
"page contains a packet too short to be valid")
(self.channels, self.sample_rate, max_bitrate, nominal_bitrate, (self.channels, self.sample_rate, max_bitrate, nominal_bitrate,
min_bitrate) = struct.unpack("<B4i", page.packets[0][11:28]) min_bitrate) = struct.unpack("<BI3i", page.packets[0][11:28])
if self.sample_rate == 0:
raise OggVorbisHeaderError("sample rate can't be zero")
self.serial = page.serial self.serial = page.serial
max_bitrate = max(0, max_bitrate) max_bitrate = max(0, max_bitrate)
@@ -83,13 +90,13 @@ class OggVorbisInfo(StreamInfo):
def _post_tags(self, fileobj): def _post_tags(self, fileobj):
"""Raises ogg.error""" """Raises ogg.error"""
page = OggPage.find_last(fileobj, self.serial) page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None: if page is None:
raise OggVorbisHeaderError raise OggVorbisHeaderError
self.length = page.position / float(self.sample_rate) self.length = page.position / float(self.sample_rate)
def pprint(self): def pprint(self):
return "Ogg Vorbis, %.2f seconds, %d bps" % ( return u"Ogg Vorbis, %.2f seconds, %d bps" % (
self.length, self.bitrate) self.length, self.bitrate)
Executable → Regular
+27 -5
View File
@@ -22,12 +22,23 @@ __all__ = ["OptimFROG", "Open", "delete"]
import struct import struct
from ._compat import endswith from ._util import convert_error, endswith
from ._util import convert_error
from mutagen import StreamInfo from mutagen import StreamInfo
from mutagen.apev2 import APEv2File, error, delete from mutagen.apev2 import APEv2File, error, delete
SAMPLE_TYPE_BITS = {
0: 8,
1: 8,
2: 16,
3: 16,
4: 24,
5: 24,
6: 32,
7: 32,
}
class OptimFROGHeaderError(error): class OptimFROGHeaderError(error):
pass pass
@@ -41,6 +52,8 @@ class OptimFROGInfo(StreamInfo):
channels (`int`): number of audio channels channels (`int`): number of audio channels
length (`float`): file length in seconds, as a float length (`float`): file length in seconds, as a float
sample_rate (`int`): audio sampling rate in Hz sample_rate (`int`): audio sampling rate in Hz
bits_per_sample (`int`): the audio sample size
encoder_info (`mutagen.text`): encoder version, e.g. "5.100"
""" """
@convert_error(IOError, OptimFROGHeaderError) @convert_error(IOError, OptimFROGHeaderError)
@@ -48,21 +61,30 @@ class OptimFROGInfo(StreamInfo):
"""Raises OptimFROGHeaderError""" """Raises OptimFROGHeaderError"""
header = fileobj.read(76) header = fileobj.read(76)
if (len(header) != 76 or not header.startswith(b"OFR ") or if len(header) != 76 or not header.startswith(b"OFR "):
struct.unpack("<I", header[4:8])[0] not in [12, 15]): raise OptimFROGHeaderError("not an OptimFROG file")
data_size = struct.unpack("<I", header[4:8])[0]
if data_size != 12 and data_size < 15:
raise OptimFROGHeaderError("not an OptimFROG file") raise OptimFROGHeaderError("not an OptimFROG file")
(total_samples, total_samples_high, sample_type, self.channels, (total_samples, total_samples_high, sample_type, self.channels,
self.sample_rate) = struct.unpack("<IHBBI", header[8:20]) self.sample_rate) = struct.unpack("<IHBBI", header[8:20])
total_samples += total_samples_high << 32 total_samples += total_samples_high << 32
self.channels += 1 self.channels += 1
self.bits_per_sample = SAMPLE_TYPE_BITS.get(sample_type)
if self.sample_rate: if self.sample_rate:
self.length = float(total_samples) / (self.channels * self.length = float(total_samples) / (self.channels *
self.sample_rate) self.sample_rate)
else: else:
self.length = 0.0 self.length = 0.0
if data_size >= 15:
encoder_id = struct.unpack("<H", header[20:22])[0]
version = str((encoder_id >> 4) + 4500)
self.encoder_info = "%s.%s" % (version[0], version[1:])
else:
self.encoder_info = ""
def pprint(self): def pprint(self):
return "OptimFROG, %.2f seconds, %d Hz" % (self.length, return u"OptimFROG, %.2f seconds, %d Hz" % (self.length,
self.sample_rate) self.sample_rate)
Executable → Regular
+4 -5
View File
@@ -12,8 +12,7 @@ import struct
from mutagen import StreamInfo, MutagenError from mutagen import StreamInfo, MutagenError
from mutagen._file import FileType from mutagen._file import FileType
from mutagen._util import loadfile from mutagen._util import loadfile, endswith
from mutagen._compat import xrange, endswith
class SMFError(MutagenError): class SMFError(MutagenError):
@@ -36,7 +35,7 @@ def _var_int(data, offset=0):
def _read_track(chunk): def _read_track(chunk):
"""Retuns a list of midi events and tempo change events""" """Retuns a list of midi events and tempo change events"""
TEMPO, MIDI = list(range(2)) TEMPO, MIDI = range(2)
# Deviations: The running status should be reset on non midi events, but # Deviations: The running status should be reset on non midi events, but
# some files contain meta events inbetween. # some files contain meta events inbetween.
@@ -91,7 +90,7 @@ def _read_track(chunk):
def _read_midi_length(fileobj): def _read_midi_length(fileobj):
"""Returns the duration in seconds. Can raise all kind of errors...""" """Returns the duration in seconds. Can raise all kind of errors..."""
TEMPO, MIDI = list(range(2)) TEMPO, MIDI = range(2)
def read_chunk(fileobj): def read_chunk(fileobj):
info = fileobj.read(8) info = fileobj.read(8)
@@ -178,7 +177,7 @@ class SMFInfo(StreamInfo):
self.length = _read_midi_length(fileobj) self.length = _read_midi_length(fileobj)
def pprint(self): def pprint(self):
return "SMF, %.2f seconds" % self.length return u"SMF, %.2f seconds" % self.length
class SMF(FileType): class SMF(FileType):
+238
View File
@@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2008 Lukáš Lalinský
# Copyright (C) 2019 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""Tom's lossless Audio Kompressor (TAK) streams with APEv2 tags.
TAK is a lossless audio compressor developed by Thomas Becker.
For more information, see:
* http://www.thbeck.de/Tak/Tak.html
* http://wiki.hydrogenaudio.org/index.php?title=TAK
"""
__all__ = ["TAK", "Open", "delete"]
import struct
from mutagen import StreamInfo
from mutagen.apev2 import (
APEv2File,
delete,
error,
)
from mutagen._util import (
BitReader,
BitReaderError,
convert_error,
enum,
endswith,
)
@enum
class TAKMetadata(object):
END = 0
STREAM_INFO = 1
SEEK_TABLE = 2 # Removed in TAK 1.1.1
SIMPLE_WAVE_DATA = 3
ENCODER_INFO = 4
UNUSED_SPACE = 5 # New in TAK 1.0.3
MD5 = 6 # New in TAK 1.1.1
LAST_FRAME_INFO = 7 # New in TAK 1.1.1
CRC_SIZE = 3
ENCODER_INFO_CODEC_BITS = 6
ENCODER_INFO_PROFILE_BITS = 4
ENCODER_INFO_TOTAL_BITS = ENCODER_INFO_CODEC_BITS + ENCODER_INFO_PROFILE_BITS
SIZE_INFO_FRAME_DURATION_BITS = 4
SIZE_INFO_SAMPLE_NUM_BITS = 35
SIZE_INFO_TOTAL_BITS = (SIZE_INFO_FRAME_DURATION_BITS
+ SIZE_INFO_SAMPLE_NUM_BITS)
AUDIO_FORMAT_DATA_TYPE_BITS = 3
AUDIO_FORMAT_SAMPLE_RATE_BITS = 18
AUDIO_FORMAT_SAMPLE_BITS_BITS = 5
AUDIO_FORMAT_CHANNEL_NUM_BITS = 4
AUDIO_FORMAT_HAS_EXTENSION_BITS = 1
AUDIO_FORMAT_BITS_MIN = 31
AUDIO_FORMAT_BITS_MAX = 31 + 102
SAMPLE_RATE_MIN = 6000
SAMPLE_BITS_MIN = 8
CHANNEL_NUM_MIN = 1
STREAM_INFO_BITS_MIN = (ENCODER_INFO_TOTAL_BITS
+ SIZE_INFO_TOTAL_BITS
+ AUDIO_FORMAT_BITS_MIN)
STREAM_INFO_BITS_MAX = (ENCODER_INFO_TOTAL_BITS
+ SIZE_INFO_TOTAL_BITS
+ AUDIO_FORMAT_BITS_MAX)
STREAM_INFO_SIZE_MIN = (STREAM_INFO_BITS_MIN + 7) / 8
STREAM_INFO_SIZE_MAX = (STREAM_INFO_BITS_MAX + 7) / 8
class _LSBBitReader(BitReader):
"""BitReader implementation which reads bits starting at LSB in each byte.
"""
def _lsb(self, count):
value = self._buffer & 0xff >> (8 - count)
self._buffer = self._buffer >> count
self._bits -= count
return value
def bits(self, count):
"""Reads `count` bits and returns an uint, LSB read first.
May raise BitReaderError if not enough data could be read or
IOError by the underlying file object.
"""
if count < 0:
raise ValueError
value = 0
if count <= self._bits:
value = self._lsb(count)
else:
# First read all available bits
shift = 0
remaining = count
if self._bits > 0:
remaining -= self._bits
shift = self._bits
value = self._lsb(self._bits)
assert self._bits == 0
# Now add additional bytes
n_bytes = (remaining - self._bits + 7) // 8
data = self._fileobj.read(n_bytes)
if len(data) != n_bytes:
raise BitReaderError("not enough data")
for b in bytearray(data):
if remaining > 8: # Use full byte
remaining -= 8
value = (b << shift) | value
shift += 8
else:
self._buffer = b
self._bits = 8
b = self._lsb(remaining)
value = (b << shift) | value
assert 0 <= self._bits < 8
return value
class TAKHeaderError(error):
pass
class TAKInfo(StreamInfo):
"""TAK stream information.
Attributes:
channels (`int`): number of audio channels
length (`float`): file length in seconds, as a float
sample_rate (`int`): audio sampling rate in Hz
bits_per_sample (`int`): audio sample size
encoder_info (`mutagen.text`): encoder version
"""
channels = 0
length = 0
sample_rate = 0
bitrate = 0
encoder_info = ""
@convert_error(IOError, TAKHeaderError)
@convert_error(BitReaderError, TAKHeaderError)
def __init__(self, fileobj):
stream_id = fileobj.read(4)
if len(stream_id) != 4 or not stream_id == b"tBaK":
raise TAKHeaderError("not a TAK file")
bitreader = _LSBBitReader(fileobj)
while True:
type = TAKMetadata(bitreader.bits(7))
bitreader.skip(1) # Unused
size = struct.unpack("<I", bitreader.bytes(3) + b'\0')[0]
data_size = size - CRC_SIZE
pos = fileobj.tell()
if type == TAKMetadata.END:
break
elif type == TAKMetadata.STREAM_INFO:
self._parse_stream_info(bitreader, size)
elif type == TAKMetadata.ENCODER_INFO:
self._parse_encoder_info(bitreader, data_size)
assert bitreader.is_aligned()
fileobj.seek(pos + size)
if self.sample_rate > 0:
self.length = self.number_of_samples / float(self.sample_rate)
def _parse_stream_info(self, bitreader, size):
if size < STREAM_INFO_SIZE_MIN or size > STREAM_INFO_SIZE_MAX:
raise TAKHeaderError("stream info has invalid length")
# Encoder Info
bitreader.skip(ENCODER_INFO_CODEC_BITS)
bitreader.skip(ENCODER_INFO_PROFILE_BITS)
# Size Info
bitreader.skip(SIZE_INFO_FRAME_DURATION_BITS)
self.number_of_samples = bitreader.bits(SIZE_INFO_SAMPLE_NUM_BITS)
# Audio Format
bitreader.skip(AUDIO_FORMAT_DATA_TYPE_BITS)
self.sample_rate = (bitreader.bits(AUDIO_FORMAT_SAMPLE_RATE_BITS)
+ SAMPLE_RATE_MIN)
self.bits_per_sample = (bitreader.bits(AUDIO_FORMAT_SAMPLE_BITS_BITS)
+ SAMPLE_BITS_MIN)
self.channels = (bitreader.bits(AUDIO_FORMAT_CHANNEL_NUM_BITS)
+ CHANNEL_NUM_MIN)
bitreader.skip(AUDIO_FORMAT_HAS_EXTENSION_BITS)
def _parse_encoder_info(self, bitreader, size):
patch = bitreader.bits(8)
minor = bitreader.bits(8)
major = bitreader.bits(8)
self.encoder_info = "TAK %d.%d.%d" % (major, minor, patch)
def pprint(self):
return u"%s, %d Hz, %d bits, %.2f seconds, %d channel(s)" % (
self.encoder_info or "TAK", self.sample_rate, self.bits_per_sample,
self.length, self.channels)
class TAK(APEv2File):
"""TAK(filething)
Arguments:
filething (filething)
Attributes:
info (`TAKInfo`)
"""
_Info = TAKInfo
_mimes = ["audio/x-tak"]
@staticmethod
def score(filename, fileobj, header):
return header.startswith(b"tBaK") + endswith(filename.lower(), ".tak")
Open = TAK
Executable → Regular
+3 -4
View File
@@ -10,17 +10,16 @@
True Audio is a lossless format designed for real-time encoding and True Audio is a lossless format designed for real-time encoding and
decoding. This module is based on the documentation at decoding. This module is based on the documentation at
http://www.true-audio.com/TTA_Lossless_Audio_Codec\_-_Format_Description http://www.true-audio.com/TTA_Lossless_Audio_Codec\\_-_Format_Description
True Audio files use ID3 tags. True Audio files use ID3 tags.
""" """
__all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"] __all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"]
from ._compat import endswith
from mutagen import StreamInfo from mutagen import StreamInfo
from mutagen.id3 import ID3FileType, delete from mutagen.id3 import ID3FileType, delete
from mutagen._util import cdata, MutagenError, convert_error from mutagen._util import cdata, MutagenError, convert_error, endswith
class error(MutagenError): class error(MutagenError):
@@ -54,7 +53,7 @@ class TrueAudioInfo(StreamInfo):
self.length = float(samples) / self.sample_rate self.length = float(samples) / self.sample_rate
def pprint(self): def pprint(self):
return "True Audio, %.2f seconds, %d Hz." % ( return u"True Audio, %.2f seconds, %d Hz." % (
self.length, self.sample_rate) self.length, self.sample_rate)
+210
View File
@@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017 Borewit
# Copyright (C) 2019-2020 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""Microsoft WAVE/RIFF audio file/stream information and tags."""
import sys
import struct
from mutagen import StreamInfo, FileType
from mutagen.id3 import ID3
from mutagen._riff import RiffFile, InvalidChunk
from mutagen._iff import error as IffError
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import (
convert_error,
endswith,
loadfile,
reraise,
)
__all__ = ["WAVE", "Open", "delete"]
class error(IffError):
"""WAVE stream parsing errors."""
class _WaveFile(RiffFile):
"""Representation of a RIFF/WAVE file"""
def __init__(self, fileobj):
RiffFile.__init__(self, fileobj)
if self.file_type != u'WAVE':
raise error("Expected RIFF/WAVE.")
# Normalize ID3v2-tag-chunk to lowercase
if u'ID3' in self:
self[u'ID3'].id = u'id3'
class WaveStreamInfo(StreamInfo):
"""WaveStreamInfo()
Microsoft WAVE file information.
Information is parsed from the 'fmt' & 'data'chunk of the RIFF/WAVE file
Attributes:
length (`float`): audio length, in seconds
bitrate (`int`): audio bitrate, in bits per second
channels (`int`): The number of audio channels
sample_rate (`int`): audio sample rate, in Hz
bits_per_sample (`int`): The audio sample size
"""
length = 0.0
bitrate = 0
channels = 0
sample_rate = 0
bits_per_sample = 0
SIZE = 16
@convert_error(IOError, error)
def __init__(self, fileobj):
"""Raises error"""
wave_file = _WaveFile(fileobj)
try:
format_chunk = wave_file[u'fmt']
except KeyError as e:
raise error(str(e))
data = format_chunk.read()
if len(data) < 16:
raise InvalidChunk()
# RIFF: http://soundfile.sapp.org/doc/WaveFormat/
# Python struct.unpack:
# https://docs.python.org/2/library/struct.html#byte-order-size-and-alignment
info = struct.unpack('<hhLLhh', data[:self.SIZE])
self.audio_format, self.channels, self.sample_rate, byte_rate, \
block_align, self.bits_per_sample = info
self.bitrate = self.channels * block_align * self.sample_rate
# Calculate duration
self._number_of_samples = 0
if block_align > 0:
try:
data_chunk = wave_file[u'data']
self._number_of_samples = data_chunk.data_size / block_align
except KeyError:
pass
if self.sample_rate > 0:
self.length = self._number_of_samples / self.sample_rate
def pprint(self):
return u"%d channel RIFF @ %d bps, %s Hz, %.2f seconds" % (
self.channels, self.bitrate, self.sample_rate, self.length)
class _WaveID3(ID3):
"""A Wave file with ID3v2 tags"""
def _pre_load_header(self, fileobj):
try:
fileobj.seek(_WaveFile(fileobj)[u'id3'].data_offset)
except (InvalidChunk, KeyError):
raise ID3NoHeaderError("No ID3 chunk")
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the Wave/RIFF file"""
fileobj = filething.fileobj
wave_file = _WaveFile(fileobj)
if u'id3' not in wave_file:
wave_file.insert_chunk(u'id3')
chunk = wave_file[u'id3']
try:
data = self._prepare_data(
fileobj, chunk.data_offset, chunk.data_size, v2_version,
v23_sep, padding)
except ID3Error as e:
reraise(error, e, sys.exc_info()[2])
chunk.resize(len(data))
chunk.write(data)
def delete(self, filething):
"""Completely removes the ID3 chunk from the RIFF/WAVE file"""
delete(filething)
self.clear()
@convert_error(IOError, error)
@loadfile(method=False, writable=True)
def delete(filething):
"""Completely removes the ID3 chunk from the RIFF/WAVE file"""
try:
_WaveFile(filething.fileobj).delete_chunk(u'id3')
except KeyError:
pass
class WAVE(FileType):
"""WAVE(filething)
A Waveform Audio File Format
(WAVE, or more commonly known as WAV due to its filename extension)
Arguments:
filething (filething)
Attributes:
tags (`mutagen.id3.ID3`)
info (`WaveStreamInfo`)
"""
_mimes = ["audio/wav", "audio/wave"]
@staticmethod
def score(filename, fileobj, header):
filename = filename.lower()
return (header.startswith(b"RIFF") + (header[8:12] == b'WAVE')
+ endswith(filename, b".wav") + endswith(filename, b".wave"))
def add_tags(self):
"""Add an empty ID3 tag to the file."""
if self.tags is None:
self.tags = _WaveID3()
else:
raise error("an ID3 tag already exists")
@convert_error(IOError, error)
@loadfile()
def load(self, filething, **kwargs):
"""Load stream and tag information from a file."""
fileobj = filething.fileobj
self.info = WaveStreamInfo(fileobj)
fileobj.seek(0, 0)
try:
self.tags = _WaveID3(fileobj, **kwargs)
except ID3NoHeaderError:
self.tags = None
except ID3Error as e:
raise error(e)
else:
self.tags.filename = self.filename
Open = WAVE
Executable → Regular
+19 -3
View File
@@ -76,9 +76,10 @@ class WavPackInfo(StreamInfo):
Attributes: Attributes:
channels (int): number of audio channels (1 or 2) channels (int): number of audio channels (1 or 2)
length (float: file length in seconds, as a float length (float): file length in seconds, as a float
sample_rate (int): audio sampling rate in Hz sample_rate (int): audio sampling rate in Hz
version (int) WavPack stream version bits_per_sample (int): audio sample size
version (int): WavPack stream version
""" """
def __init__(self, fileobj): def __init__(self, fileobj):
@@ -90,6 +91,12 @@ class WavPackInfo(StreamInfo):
self.version = header.version self.version = header.version
self.channels = bool(header.flags & 4) or 2 self.channels = bool(header.flags & 4) or 2
self.sample_rate = RATES[(header.flags >> 23) & 0xF] self.sample_rate = RATES[(header.flags >> 23) & 0xF]
self.bits_per_sample = ((header.flags & 3) + 1) * 8
# most common multiplier (DSD64)
if (header.flags >> 31) & 1:
self.sample_rate *= 4
self.bits_per_sample = 1
if header.total_samples == -1 or header.block_index != 0: if header.total_samples == -1 or header.block_index != 0:
# TODO: we could make this faster by using the tag size # TODO: we could make this faster by using the tag size
@@ -109,11 +116,20 @@ class WavPackInfo(StreamInfo):
self.length = float(samples) / self.sample_rate self.length = float(samples) / self.sample_rate
def pprint(self): def pprint(self):
return "WavPack, %.2f seconds, %d Hz" % (self.length, return u"WavPack, %.2f seconds, %d Hz" % (self.length,
self.sample_rate) self.sample_rate)
class WavPack(APEv2File): class WavPack(APEv2File):
"""WavPack(filething)
Arguments:
filething (filething)
Attributes:
info (`WavPackInfo`)
"""
_Info = WavPackInfo _Info = WavPackInfo
_mimes = ["audio/x-wavpack"] _mimes = ["audio/x-wavpack"]
+3 -3
View File
@@ -5,7 +5,7 @@
# #
# Loosely based on the API implementation from 'whatbetter', by Zachary Denton # Loosely based on the API implementation from 'whatbetter', by Zachary Denton
# See https://github.com/zacharydenton/whatbetter # See https://github.com/zacharydenton/whatbetter
from html.parser import HTMLParser import html
import sys import sys
import json import json
@@ -219,10 +219,10 @@ class GazelleAPI(object):
else: else:
artist = Artist(id, self) artist = Artist(id, self)
if name: if name:
artist.name = HTMLParser().unescape(name) artist.name = html.unescape(name)
elif name: elif name:
artist = Artist(-1, self) artist = Artist(-1, self)
artist.name = HTMLParser().unescape(name) artist.name = html.unescape(name)
else: else:
raise Exception("You must specify either an ID or a Name to get an artist.") raise Exception("You must specify either an ID or a Name to get an artist.")
+3 -3
View File
@@ -1,4 +1,4 @@
from html.parser import HTMLParser import html
class InvalidArtistException(Exception): class InvalidArtistException(Exception):
pass pass
@@ -29,7 +29,7 @@ class Artist(object):
if self.id > 0: if self.id > 0:
response = self.parent_api.request(action='artist', id=self.id) response = self.parent_api.request(action='artist', id=self.id)
elif self.name: elif self.name:
self.name = HTMLParser().unescape(self.name) self.name = html.unescape(self.name)
try: try:
response = self.parent_api.request(action='artist', artistname=self.name) response = self.parent_api.request(action='artist', artistname=self.name)
except Exception: except Exception:
@@ -47,7 +47,7 @@ class Artist(object):
self.id = artist_json_response['id'] self.id = artist_json_response['id']
self.parent_api.cached_artists[self.id] = self self.parent_api.cached_artists[self.id] = self
self.name = HTMLParser().unescape(artist_json_response['name']) self.name = html.unescape(artist_json_response['name'])
self.notifications_enabled = artist_json_response['notificationsEnabled'] self.notifications_enabled = artist_json_response['notificationsEnabled']
self.has_bookmarked = artist_json_response['hasBookmarked'] self.has_bookmarked = artist_json_response['hasBookmarked']
self.image = artist_json_response['image'] self.image = artist_json_response['image']