mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-09 16:22:52 +01:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaf2db6c59 | ||
|
|
586b9ed3c8 | ||
|
|
d89f4171da | ||
|
|
9f7be5348b | ||
|
|
9c254ff222 | ||
|
|
ba969fd3b8 | ||
|
|
c851d5ed1a | ||
|
|
2223928958 | ||
|
|
164c3cacbc | ||
|
|
16d4ac8895 | ||
|
|
f4d60226b3 | ||
|
|
9ca87e23b2 | ||
|
|
d934c865c6 | ||
|
|
de74cd2502 | ||
|
|
f41db714a9 | ||
|
|
f03b82e5f6 | ||
|
|
e2db680b9e | ||
|
|
a3db89c11d | ||
|
|
517d0eb327 | ||
|
|
2bacd5a0fc | ||
|
|
5a559c526d | ||
|
|
095cee9368 | ||
|
|
79cb133d1d | ||
|
|
3a9b749017 | ||
|
|
0182be2f27 | ||
|
|
b6388f7daa | ||
|
|
b3199605be | ||
|
|
58edc604b3 | ||
|
|
379fd3d0b8 | ||
|
|
bf74f57535 | ||
|
|
f18334d87c | ||
|
|
5283b48736 | ||
|
|
dc22bb006d |
+1
-1
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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', ''),
|
||||||
@@ -327,7 +327,7 @@ class Config(object):
|
|||||||
def __init__(self, config_file):
|
def __init__(self, config_file):
|
||||||
""" Initialize the config with values from a file """
|
""" Initialize the config with values from a file """
|
||||||
self._config_file = config_file
|
self._config_file = config_file
|
||||||
self._config = ConfigParser()
|
self._config = ConfigParser(interpolation=None)
|
||||||
self._config.read(self._config_file)
|
self._config.read(self._config_file)
|
||||||
for key in list(_CONFIG_DEFINITIONS.keys()):
|
for key in list(_CONFIG_DEFINITIONS.keys()):
|
||||||
self.check_setting(key)
|
self.check_setting(key)
|
||||||
@@ -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)
|
||||||
@@ -376,7 +376,7 @@ class Config(object):
|
|||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
""" Make a copy of the stored config and write it to the configured file """
|
""" Make a copy of the stored config and write it to the configured file """
|
||||||
new_config = ConfigParser()
|
new_config = ConfigParser(interpolation=None)
|
||||||
|
|
||||||
# first copy over everything from the old config, even if it is not
|
# first copy over everything from the old config, even if it is not
|
||||||
# correctly defined to keep from losing data
|
# correctly defined to keep from losing data
|
||||||
|
|||||||
+1
-1
@@ -117,7 +117,7 @@ class DBConnection:
|
|||||||
break
|
break
|
||||||
|
|
||||||
except sqlite3.OperationalError as e:
|
except sqlite3.OperationalError as e:
|
||||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
if "unable to open database file" in str(e) or "database is locked" in str(e):
|
||||||
dberror = e
|
dberror = e
|
||||||
if args is None:
|
if args is None:
|
||||||
logger.debug('Database error: %s. Query: %s', e, query)
|
logger.debug('Database error: %s. Query: %s', e, query)
|
||||||
|
|||||||
+1
-20
@@ -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()
|
||||||
|
|||||||
+44
-30
@@ -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,7 +52,14 @@ 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
|
||||||
"""
|
"""
|
||||||
return (x > y) - (x < y)
|
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)
|
||||||
|
|
||||||
def multikeysort(items, columns):
|
def multikeysort(items, columns):
|
||||||
comparers = [
|
comparers = [
|
||||||
@@ -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
|
||||||
@@ -1036,3 +1043,10 @@ def capture_beets_log(logger='beets'):
|
|||||||
yield capture.messages
|
yield capture.messages
|
||||||
finally:
|
finally:
|
||||||
log.removeHandler(capture)
|
log.removeHandler(capture)
|
||||||
|
|
||||||
|
def have_pct_have_total(db_artist):
|
||||||
|
have_tracks = db_artist['HaveTracks'] or 0
|
||||||
|
total_tracks = db_artist['TotalTracks'] or 0
|
||||||
|
have_pct = have_tracks / total_tracks if total_tracks else 0
|
||||||
|
return (have_pct, total_tracks)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+15
-19
@@ -102,12 +102,13 @@ def artistlist_to_mbids(artistlist, forced=False):
|
|||||||
myDB.action('DELETE from newartists WHERE ArtistName=?', [artist])
|
myDB.action('DELETE from newartists WHERE ArtistName=?', [artist])
|
||||||
|
|
||||||
# Update the similar artist tag cloud:
|
# Update the similar artist tag cloud:
|
||||||
logger.info('Updating artist information from Last.fm')
|
# TODO: Fix last.fm api
|
||||||
|
# logger.info('Updating artist information from Last.fm')
|
||||||
|
|
||||||
try:
|
# try:
|
||||||
lastfm.getSimilar()
|
# lastfm.getSimilar()
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
logger.warn('Failed to update artist information from Last.fm: %s' % e)
|
# logger.warn('Failed to update artist information from Last.fm: %s' % e)
|
||||||
|
|
||||||
|
|
||||||
def addArtistIDListToDB(artistidlist):
|
def addArtistIDListToDB(artistidlist):
|
||||||
@@ -245,7 +246,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()
|
||||||
|
|
||||||
@@ -274,18 +275,18 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
if len(check_release_date) == 10:
|
if len(check_release_date) == 10:
|
||||||
release_date = check_release_date
|
release_date = check_release_date
|
||||||
elif len(check_release_date) == 7:
|
elif len(check_release_date) == 7:
|
||||||
release_date = check_release_date + "-31"
|
release_date = check_release_date + "-27"
|
||||||
elif len(check_release_date) == 4:
|
elif len(check_release_date) == 4:
|
||||||
release_date = check_release_date + "-12-31"
|
release_date = check_release_date + "-12-27"
|
||||||
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,14 +451,9 @@ 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
|
newValueDict['Status'] = "Wanted"
|
||||||
# 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"
|
|
||||||
else:
|
else:
|
||||||
newValueDict['Status'] = "Skipped"
|
newValueDict['Status'] = "Skipped"
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+12
-30
@@ -14,20 +14,14 @@
|
|||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from headphones import logger, db, helpers
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import musicbrainzngs
|
||||||
|
|
||||||
import headphones
|
import headphones
|
||||||
import musicbrainzngs
|
|
||||||
import headphones.lock
|
import headphones.lock
|
||||||
|
from headphones import logger, db, helpers
|
||||||
|
|
||||||
try:
|
|
||||||
# pylint:disable=E0611
|
|
||||||
# ignore this error because we are catching the ImportError
|
|
||||||
from collections import OrderedDict
|
|
||||||
# pylint:enable=E0611
|
|
||||||
except ImportError:
|
|
||||||
# Python 2.6.x fallback, from libs
|
|
||||||
from ordereddict import OrderedDict
|
|
||||||
|
|
||||||
mb_lock = headphones.lock.TimedLock(0)
|
mb_lock = headphones.lock.TimedLock(0)
|
||||||
|
|
||||||
@@ -97,7 +91,7 @@ def findArtist(name, limit=1):
|
|||||||
try:
|
try:
|
||||||
artistResults = musicbrainzngs.search_artists(limit=limit, **criteria)['artist-list']
|
artistResults = musicbrainzngs.search_artists(limit=limit, **criteria)['artist-list']
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if "at least one query term is required" in e.message:
|
if "at least one query term is required" in str(e):
|
||||||
logger.error(
|
logger.error(
|
||||||
"Tried to search without a term, or an empty one. Provided artist (probably emtpy): %s",
|
"Tried to search without a term, or an empty one. Provided artist (probably emtpy): %s",
|
||||||
name)
|
name)
|
||||||
@@ -124,11 +118,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 +127,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 +198,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 +238,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 +285,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 +346,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 +681,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 +723,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -809,7 +809,7 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
try:
|
try:
|
||||||
prev = os.umask(headphones.UMASK)
|
prev = os.umask(headphones.UMASK)
|
||||||
|
|
||||||
with open(download_path, 'w') as fp:
|
with open(download_path, 'wb') as fp:
|
||||||
fp.write(data)
|
fp.write(data)
|
||||||
|
|
||||||
os.umask(prev)
|
os.umask(prev)
|
||||||
@@ -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
|
||||||
|
|||||||
+52
-44
@@ -15,34 +15,44 @@
|
|||||||
|
|
||||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
||||||
|
|
||||||
from operator import itemgetter
|
|
||||||
import threading
|
|
||||||
import secrets
|
|
||||||
import random
|
|
||||||
import urllib.request, urllib.parse, urllib.error
|
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
from html import escape as html_escape
|
|
||||||
import urllib.request, urllib.error, urllib.parse
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers, crier
|
import secrets
|
||||||
from headphones.helpers import checked, radio, today, clean_name
|
import sys
|
||||||
from mako.lookup import TemplateLookup
|
import threading
|
||||||
from mako import exceptions
|
import time
|
||||||
import headphones
|
from collections import OrderedDict
|
||||||
import cherrypy
|
from html import escape as html_escape
|
||||||
|
from operator import itemgetter
|
||||||
|
from urllib import parse
|
||||||
|
|
||||||
try:
|
import cherrypy
|
||||||
# pylint:disable=E0611
|
from mako import exceptions
|
||||||
# ignore this error because we are catching the ImportError
|
from mako.lookup import TemplateLookup
|
||||||
from collections import OrderedDict
|
|
||||||
# pylint:enable=E0611
|
import headphones
|
||||||
except ImportError:
|
from headphones import (
|
||||||
# Python 2.6.x fallback, from libs
|
crier,
|
||||||
from ordereddict import OrderedDict
|
db,
|
||||||
|
importer,
|
||||||
|
lastfm,
|
||||||
|
librarysync,
|
||||||
|
logger,
|
||||||
|
mb,
|
||||||
|
notifiers,
|
||||||
|
searcher,
|
||||||
|
)
|
||||||
|
from headphones.helpers import (
|
||||||
|
checked,
|
||||||
|
clean_name,
|
||||||
|
have_pct_have_total,
|
||||||
|
pattern_substitute,
|
||||||
|
radio,
|
||||||
|
replace_illegal_chars,
|
||||||
|
today,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def serve_template(templatename, **kwargs):
|
def serve_template(templatename, **kwargs):
|
||||||
@@ -326,9 +336,9 @@ class WebInterface(object):
|
|||||||
'$first': firstchar.lower(),
|
'$first': firstchar.lower(),
|
||||||
}
|
}
|
||||||
|
|
||||||
folder = helpers.pattern_substitute(folder_format.strip(), values, normalize=True)
|
folder = pattern_substitute(folder_format.strip(), values, normalize=True)
|
||||||
|
|
||||||
folder = helpers.replace_illegal_chars(folder, type="folder")
|
folder = replace_illegal_chars(folder, type="folder")
|
||||||
folder = folder.replace('./', '_/').replace('/.', '/_')
|
folder = folder.replace('./', '_/').replace('/.', '/_')
|
||||||
|
|
||||||
if folder.endswith('.'):
|
if folder.endswith('.'):
|
||||||
@@ -461,7 +471,7 @@ class WebInterface(object):
|
|||||||
def download_specific_release(self, AlbumID, title, size, url, provider, kind, **kwargs):
|
def download_specific_release(self, AlbumID, title, size, url, provider, kind, **kwargs):
|
||||||
# Handle situations where the torrent url contains arguments that are parsed
|
# Handle situations where the torrent url contains arguments that are parsed
|
||||||
if kwargs:
|
if kwargs:
|
||||||
url = urllib.parse.quote(url, safe=":?/=&") + '&' + urllib.parse.urlencode(kwargs)
|
url = parse.quote(url, safe=":?/=&") + '&' + parse.urlencode(kwargs)
|
||||||
try:
|
try:
|
||||||
result = [(title, int(size), url, provider, kind)]
|
result = [(title, int(size), url, provider, kind)]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -586,7 +596,7 @@ class WebInterface(object):
|
|||||||
for albums in have_albums:
|
for albums in have_albums:
|
||||||
# Have to skip over manually matched tracks
|
# Have to skip over manually matched tracks
|
||||||
if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']:
|
if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']:
|
||||||
original_clean = helpers.clean_name(
|
original_clean = clean_name(
|
||||||
albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
|
albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
|
||||||
# else:
|
# else:
|
||||||
# original_clean = None
|
# original_clean = None
|
||||||
@@ -633,8 +643,8 @@ class WebInterface(object):
|
|||||||
(artist, album))
|
(artist, album))
|
||||||
|
|
||||||
elif action == "matchArtist":
|
elif action == "matchArtist":
|
||||||
existing_artist_clean = helpers.clean_name(existing_artist).lower()
|
existing_artist_clean = clean_name(existing_artist).lower()
|
||||||
new_artist_clean = helpers.clean_name(new_artist).lower()
|
new_artist_clean = clean_name(new_artist).lower()
|
||||||
if new_artist_clean != existing_artist_clean:
|
if new_artist_clean != existing_artist_clean:
|
||||||
have_tracks = myDB.action(
|
have_tracks = myDB.action(
|
||||||
'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?',
|
'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?',
|
||||||
@@ -678,10 +688,10 @@ class WebInterface(object):
|
|||||||
"Artist %s already named appropriately; nothing to modify" % existing_artist)
|
"Artist %s already named appropriately; nothing to modify" % existing_artist)
|
||||||
|
|
||||||
elif action == "matchAlbum":
|
elif action == "matchAlbum":
|
||||||
existing_artist_clean = helpers.clean_name(existing_artist).lower()
|
existing_artist_clean = clean_name(existing_artist).lower()
|
||||||
new_artist_clean = helpers.clean_name(new_artist).lower()
|
new_artist_clean = clean_name(new_artist).lower()
|
||||||
existing_album_clean = helpers.clean_name(existing_album).lower()
|
existing_album_clean = clean_name(existing_album).lower()
|
||||||
new_album_clean = helpers.clean_name(new_album).lower()
|
new_album_clean = clean_name(new_album).lower()
|
||||||
existing_clean_string = existing_artist_clean + " " + existing_album_clean
|
existing_clean_string = existing_artist_clean + " " + existing_album_clean
|
||||||
new_clean_string = new_artist_clean + " " + new_album_clean
|
new_clean_string = new_artist_clean + " " + new_album_clean
|
||||||
if existing_clean_string != new_clean_string:
|
if existing_clean_string != new_clean_string:
|
||||||
@@ -737,7 +747,7 @@ class WebInterface(object):
|
|||||||
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have')
|
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have')
|
||||||
for albums in manualalbums:
|
for albums in manualalbums:
|
||||||
if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']:
|
if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']:
|
||||||
original_clean = helpers.clean_name(
|
original_clean = clean_name(
|
||||||
albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
|
albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
|
||||||
if albums['Matched'] == "Ignored" or albums['Matched'] == "Manual" or albums[
|
if albums['Matched'] == "Ignored" or albums['Matched'] == "Manual" or albums[
|
||||||
'CleanName'] != original_clean:
|
'CleanName'] != original_clean:
|
||||||
@@ -778,7 +788,7 @@ class WebInterface(object):
|
|||||||
[artist])
|
[artist])
|
||||||
update_count = 0
|
update_count = 0
|
||||||
for tracks in update_clean:
|
for tracks in update_clean:
|
||||||
original_clean = helpers.clean_name(
|
original_clean = clean_name(
|
||||||
tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[
|
tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[
|
||||||
'TrackTitle']).lower()
|
'TrackTitle']).lower()
|
||||||
album = tracks['AlbumTitle']
|
album = tracks['AlbumTitle']
|
||||||
@@ -810,7 +820,7 @@ class WebInterface(object):
|
|||||||
(artist, album))
|
(artist, album))
|
||||||
update_count = 0
|
update_count = 0
|
||||||
for tracks in update_clean:
|
for tracks in update_clean:
|
||||||
original_clean = helpers.clean_name(
|
original_clean = clean_name(
|
||||||
tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[
|
tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[
|
||||||
'TrackTitle']).lower()
|
'TrackTitle']).lower()
|
||||||
track_title = tracks['TrackTitle']
|
track_title = tracks['TrackTitle']
|
||||||
@@ -1018,9 +1028,7 @@ class WebInterface(object):
|
|||||||
totalcount = myDB.select('SELECT COUNT(*) from artists')[0][0]
|
totalcount = myDB.select('SELECT COUNT(*) from artists')[0][0]
|
||||||
|
|
||||||
if sortbyhavepercent:
|
if sortbyhavepercent:
|
||||||
filtered.sort(key=lambda x: (
|
filtered.sort(key=have_pct_have_total, reverse=sSortDir_0 == "asc")
|
||||||
float(x['HaveTracks']) / x['TotalTracks'] if x['TotalTracks'] > 0 else 0.0,
|
|
||||||
x['HaveTracks'] if x['HaveTracks'] else 0.0), reverse=sSortDir_0 == "asc")
|
|
||||||
|
|
||||||
# can't figure out how to change the datatables default sorting order when its using an ajax datasource so ill
|
# can't figure out how to change the datatables default sorting order when its using an ajax datasource so ill
|
||||||
# just reverse it here and the first click on the "Latest Album" header will sort by descending release date
|
# just reverse it here and the first click on the "Latest Album" header will sort by descending release date
|
||||||
@@ -1677,16 +1685,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,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,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
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Read resources contained within a package."""
|
||||||
|
|
||||||
|
from ._common import (
|
||||||
|
as_file,
|
||||||
|
files,
|
||||||
|
Package,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ._legacy import (
|
||||||
|
contents,
|
||||||
|
open_binary,
|
||||||
|
read_binary,
|
||||||
|
open_text,
|
||||||
|
read_text,
|
||||||
|
is_resource,
|
||||||
|
path,
|
||||||
|
Resource,
|
||||||
|
)
|
||||||
|
|
||||||
|
from importlib_resources.abc import ResourceReader
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Package',
|
||||||
|
'Resource',
|
||||||
|
'ResourceReader',
|
||||||
|
'as_file',
|
||||||
|
'contents',
|
||||||
|
'files',
|
||||||
|
'is_resource',
|
||||||
|
'open_binary',
|
||||||
|
'open_text',
|
||||||
|
'path',
|
||||||
|
'read_binary',
|
||||||
|
'read_text',
|
||||||
|
]
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
from contextlib import suppress
|
||||||
|
from io import TextIOWrapper
|
||||||
|
|
||||||
|
from . import abc
|
||||||
|
|
||||||
|
|
||||||
|
class SpecLoaderAdapter:
|
||||||
|
"""
|
||||||
|
Adapt a package spec to adapt the underlying loader.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, spec, adapter=lambda spec: spec.loader):
|
||||||
|
self.spec = spec
|
||||||
|
self.loader = adapter(spec)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self.spec, name)
|
||||||
|
|
||||||
|
|
||||||
|
class TraversableResourcesLoader:
|
||||||
|
"""
|
||||||
|
Adapt a loader to provide TraversableResources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, spec):
|
||||||
|
self.spec = spec
|
||||||
|
|
||||||
|
def get_resource_reader(self, name):
|
||||||
|
return CompatibilityFiles(self.spec)._native()
|
||||||
|
|
||||||
|
|
||||||
|
def _io_wrapper(file, mode='r', *args, **kwargs):
|
||||||
|
if mode == 'r':
|
||||||
|
return TextIOWrapper(file, *args, **kwargs)
|
||||||
|
elif mode == 'rb':
|
||||||
|
return file
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibilityFiles:
|
||||||
|
"""
|
||||||
|
Adapter for an existing or non-existent resource reader
|
||||||
|
to provide a compatibility .files().
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SpecPath(abc.Traversable):
|
||||||
|
"""
|
||||||
|
Path tied to a module spec.
|
||||||
|
Can be read and exposes the resource reader children.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, spec, reader):
|
||||||
|
self._spec = spec
|
||||||
|
self._reader = reader
|
||||||
|
|
||||||
|
def iterdir(self):
|
||||||
|
if not self._reader:
|
||||||
|
return iter(())
|
||||||
|
return iter(
|
||||||
|
CompatibilityFiles.ChildPath(self._reader, path)
|
||||||
|
for path in self._reader.contents()
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_file(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
is_dir = is_file
|
||||||
|
|
||||||
|
def joinpath(self, other):
|
||||||
|
if not self._reader:
|
||||||
|
return CompatibilityFiles.OrphanPath(other)
|
||||||
|
return CompatibilityFiles.ChildPath(self._reader, other)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._spec.name
|
||||||
|
|
||||||
|
def open(self, mode='r', *args, **kwargs):
|
||||||
|
return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs)
|
||||||
|
|
||||||
|
class ChildPath(abc.Traversable):
|
||||||
|
"""
|
||||||
|
Path tied to a resource reader child.
|
||||||
|
Can be read but doesn't expose any meaningful children.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, reader, name):
|
||||||
|
self._reader = reader
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
def iterdir(self):
|
||||||
|
return iter(())
|
||||||
|
|
||||||
|
def is_file(self):
|
||||||
|
return self._reader.is_resource(self.name)
|
||||||
|
|
||||||
|
def is_dir(self):
|
||||||
|
return not self.is_file()
|
||||||
|
|
||||||
|
def joinpath(self, other):
|
||||||
|
return CompatibilityFiles.OrphanPath(self.name, other)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def open(self, mode='r', *args, **kwargs):
|
||||||
|
return _io_wrapper(
|
||||||
|
self._reader.open_resource(self.name), mode, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
class OrphanPath(abc.Traversable):
|
||||||
|
"""
|
||||||
|
Orphan path, not tied to a module spec or resource reader.
|
||||||
|
Can't be read and doesn't expose any meaningful children.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *path_parts):
|
||||||
|
if len(path_parts) < 1:
|
||||||
|
raise ValueError('Need at least one path part to construct a path')
|
||||||
|
self._path = path_parts
|
||||||
|
|
||||||
|
def iterdir(self):
|
||||||
|
return iter(())
|
||||||
|
|
||||||
|
def is_file(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
is_dir = is_file
|
||||||
|
|
||||||
|
def joinpath(self, other):
|
||||||
|
return CompatibilityFiles.OrphanPath(*self._path, other)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._path[-1]
|
||||||
|
|
||||||
|
def open(self, mode='r', *args, **kwargs):
|
||||||
|
raise FileNotFoundError("Can't open orphan path")
|
||||||
|
|
||||||
|
def __init__(self, spec):
|
||||||
|
self.spec = spec
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _reader(self):
|
||||||
|
with suppress(AttributeError):
|
||||||
|
return self.spec.loader.get_resource_reader(self.spec.name)
|
||||||
|
|
||||||
|
def _native(self):
|
||||||
|
"""
|
||||||
|
Return the native reader if it supports files().
|
||||||
|
"""
|
||||||
|
reader = self._reader
|
||||||
|
return reader if hasattr(reader, 'files') else self
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._reader, attr)
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
return CompatibilityFiles.SpecPath(self.spec, self._reader)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_spec(package):
|
||||||
|
"""
|
||||||
|
Construct a package spec with traversable compatibility
|
||||||
|
on the spec/loader/reader.
|
||||||
|
"""
|
||||||
|
return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
import functools
|
||||||
|
import contextlib
|
||||||
|
import types
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from typing import Union, Optional
|
||||||
|
from .abc import ResourceReader, Traversable
|
||||||
|
|
||||||
|
from ._compat import wrap_spec
|
||||||
|
|
||||||
|
Package = Union[types.ModuleType, str]
|
||||||
|
|
||||||
|
|
||||||
|
def files(package):
|
||||||
|
# type: (Package) -> Traversable
|
||||||
|
"""
|
||||||
|
Get a Traversable resource from a package
|
||||||
|
"""
|
||||||
|
return from_package(get_package(package))
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_reader(package):
|
||||||
|
# type: (types.ModuleType) -> Optional[ResourceReader]
|
||||||
|
"""
|
||||||
|
Return the package's loader if it's a ResourceReader.
|
||||||
|
"""
|
||||||
|
# We can't use
|
||||||
|
# a issubclass() check here because apparently abc.'s __subclasscheck__()
|
||||||
|
# hook wants to create a weak reference to the object, but
|
||||||
|
# zipimport.zipimporter does not support weak references, resulting in a
|
||||||
|
# TypeError. That seems terrible.
|
||||||
|
spec = package.__spec__
|
||||||
|
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore
|
||||||
|
if reader is None:
|
||||||
|
return None
|
||||||
|
return reader(spec.name) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(cand):
|
||||||
|
# type: (Package) -> types.ModuleType
|
||||||
|
return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand)
|
||||||
|
|
||||||
|
|
||||||
|
def get_package(package):
|
||||||
|
# type: (Package) -> types.ModuleType
|
||||||
|
"""Take a package name or module object and return the module.
|
||||||
|
|
||||||
|
Raise an exception if the resolved module is not a package.
|
||||||
|
"""
|
||||||
|
resolved = resolve(package)
|
||||||
|
if wrap_spec(resolved).submodule_search_locations is None:
|
||||||
|
raise TypeError(f'{package!r} is not a package')
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def from_package(package):
|
||||||
|
"""
|
||||||
|
Return a Traversable object for the given package.
|
||||||
|
|
||||||
|
"""
|
||||||
|
spec = wrap_spec(package)
|
||||||
|
reader = spec.loader.get_resource_reader(spec.name)
|
||||||
|
return reader.files()
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _tempfile(reader, suffix=''):
|
||||||
|
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
|
||||||
|
# blocks due to the need to close the temporary file to work on Windows
|
||||||
|
# properly.
|
||||||
|
fd, raw_path = tempfile.mkstemp(suffix=suffix)
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
os.write(fd, reader())
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
del reader
|
||||||
|
yield pathlib.Path(raw_path)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.remove(raw_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@functools.singledispatch
|
||||||
|
def as_file(path):
|
||||||
|
"""
|
||||||
|
Given a Traversable object, return that object as a
|
||||||
|
path on the local file system in a context manager.
|
||||||
|
"""
|
||||||
|
return _tempfile(path.read_bytes, suffix=path.name)
|
||||||
|
|
||||||
|
|
||||||
|
@as_file.register(pathlib.Path)
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _(path):
|
||||||
|
"""
|
||||||
|
Degenerate behavior for pathlib.Path objects.
|
||||||
|
"""
|
||||||
|
yield path
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# flake8: noqa
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
from zipfile import Path as ZipPath # type: ignore
|
||||||
|
else:
|
||||||
|
from zipp import Path as ZipPath # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import runtime_checkable # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def runtime_checkable(cls): # type: ignore
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Protocol # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
Protocol = abc.ABC # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class TraversableResourcesLoader:
|
||||||
|
"""
|
||||||
|
Adapt loaders to provide TraversableResources and other
|
||||||
|
compatibility.
|
||||||
|
|
||||||
|
Used primarily for Python 3.9 and earlier where the native
|
||||||
|
loaders do not yet implement TraversableResources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, spec):
|
||||||
|
self.spec = spec
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self.spec.origin
|
||||||
|
|
||||||
|
def get_resource_reader(self, name):
|
||||||
|
from . import readers, _adapters
|
||||||
|
|
||||||
|
def _zip_reader(spec):
|
||||||
|
with suppress(AttributeError):
|
||||||
|
return readers.ZipReader(spec.loader, spec.name)
|
||||||
|
|
||||||
|
def _namespace_reader(spec):
|
||||||
|
with suppress(AttributeError, ValueError):
|
||||||
|
return readers.NamespaceReader(spec.submodule_search_locations)
|
||||||
|
|
||||||
|
def _available_reader(spec):
|
||||||
|
with suppress(AttributeError):
|
||||||
|
return spec.loader.get_resource_reader(spec.name)
|
||||||
|
|
||||||
|
def _native_reader(spec):
|
||||||
|
reader = _available_reader(spec)
|
||||||
|
return reader if hasattr(reader, 'files') else None
|
||||||
|
|
||||||
|
def _file_reader(spec):
|
||||||
|
try:
|
||||||
|
path = pathlib.Path(self.path)
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
if path.exists():
|
||||||
|
return readers.FileReader(self)
|
||||||
|
|
||||||
|
return (
|
||||||
|
# native reader if it supplies 'files'
|
||||||
|
_native_reader(self.spec)
|
||||||
|
or
|
||||||
|
# local ZipReader if a zip module
|
||||||
|
_zip_reader(self.spec)
|
||||||
|
or
|
||||||
|
# local NamespaceReader if a namespace module
|
||||||
|
_namespace_reader(self.spec)
|
||||||
|
or
|
||||||
|
# local FileReader
|
||||||
|
_file_reader(self.spec)
|
||||||
|
# fallback - adapt the spec ResourceReader to TraversableReader
|
||||||
|
or _adapters.CompatibilityFiles(self.spec)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_spec(package):
|
||||||
|
"""
|
||||||
|
Construct a package spec with traversable compatibility
|
||||||
|
on the spec/loader/reader.
|
||||||
|
|
||||||
|
Supersedes _adapters.wrap_spec to use TraversableResourcesLoader
|
||||||
|
from above for older Python compatibility (<3.10).
|
||||||
|
"""
|
||||||
|
from . import _adapters
|
||||||
|
|
||||||
|
return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from itertools import filterfalse
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
Callable,
|
||||||
|
Iterable,
|
||||||
|
Iterator,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Type and type variable definitions
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
_U = TypeVar('_U')
|
||||||
|
|
||||||
|
|
||||||
|
def unique_everseen(
|
||||||
|
iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = None
|
||||||
|
) -> Iterator[_T]:
|
||||||
|
"List unique elements, preserving order. Remember all elements ever seen."
|
||||||
|
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
||||||
|
# unique_everseen('ABBCcAD', str.lower) --> A B C D
|
||||||
|
seen: Set[Union[_T, _U]] = set()
|
||||||
|
seen_add = seen.add
|
||||||
|
if key is None:
|
||||||
|
for element in filterfalse(seen.__contains__, iterable):
|
||||||
|
seen_add(element)
|
||||||
|
yield element
|
||||||
|
else:
|
||||||
|
for element in iterable:
|
||||||
|
k = key(element)
|
||||||
|
if k not in seen:
|
||||||
|
seen_add(k)
|
||||||
|
yield element
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import types
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from typing import Union, Iterable, ContextManager, BinaryIO, TextIO, Any
|
||||||
|
|
||||||
|
from . import _common
|
||||||
|
|
||||||
|
Package = Union[types.ModuleType, str]
|
||||||
|
Resource = str
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
warnings.warn(
|
||||||
|
f"{func.__name__} is deprecated. Use files() instead. "
|
||||||
|
"Refer to https://importlib-resources.readthedocs.io"
|
||||||
|
"/en/latest/using.html#migrating-from-legacy for migration advice.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_path(path):
|
||||||
|
# type: (Any) -> str
|
||||||
|
"""Normalize a path by ensuring it is a string.
|
||||||
|
|
||||||
|
If the resulting string contains path separators, an exception is raised.
|
||||||
|
"""
|
||||||
|
str_path = str(path)
|
||||||
|
parent, file_name = os.path.split(str_path)
|
||||||
|
if parent:
|
||||||
|
raise ValueError(f'{path!r} must be only a file name')
|
||||||
|
return file_name
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def open_binary(package: Package, resource: Resource) -> BinaryIO:
|
||||||
|
"""Return a file-like object opened for binary reading of the resource."""
|
||||||
|
return (_common.files(package) / normalize_path(resource)).open('rb')
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def read_binary(package: Package, resource: Resource) -> bytes:
|
||||||
|
"""Return the binary contents of the resource."""
|
||||||
|
return (_common.files(package) / normalize_path(resource)).read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def open_text(
|
||||||
|
package: Package,
|
||||||
|
resource: Resource,
|
||||||
|
encoding: str = 'utf-8',
|
||||||
|
errors: str = 'strict',
|
||||||
|
) -> TextIO:
|
||||||
|
"""Return a file-like object opened for text reading of the resource."""
|
||||||
|
return (_common.files(package) / normalize_path(resource)).open(
|
||||||
|
'r', encoding=encoding, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def read_text(
|
||||||
|
package: Package,
|
||||||
|
resource: Resource,
|
||||||
|
encoding: str = 'utf-8',
|
||||||
|
errors: str = 'strict',
|
||||||
|
) -> str:
|
||||||
|
"""Return the decoded string of the resource.
|
||||||
|
|
||||||
|
The decoding-related arguments have the same semantics as those of
|
||||||
|
bytes.decode().
|
||||||
|
"""
|
||||||
|
with open_text(package, resource, encoding, errors) as fp:
|
||||||
|
return fp.read()
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def contents(package: Package) -> Iterable[str]:
|
||||||
|
"""Return an iterable of entries in `package`.
|
||||||
|
|
||||||
|
Note that not all entries are resources. Specifically, directories are
|
||||||
|
not considered resources. Use `is_resource()` on each entry returned here
|
||||||
|
to check if it is a resource or not.
|
||||||
|
"""
|
||||||
|
return [path.name for path in _common.files(package).iterdir()]
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def is_resource(package: Package, name: str) -> bool:
|
||||||
|
"""True if `name` is a resource inside `package`.
|
||||||
|
|
||||||
|
Directories are *not* resources.
|
||||||
|
"""
|
||||||
|
resource = normalize_path(name)
|
||||||
|
return any(
|
||||||
|
traversable.name == resource and traversable.is_file()
|
||||||
|
for traversable in _common.files(package).iterdir()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def path(
|
||||||
|
package: Package,
|
||||||
|
resource: Resource,
|
||||||
|
) -> ContextManager[pathlib.Path]:
|
||||||
|
"""A context manager providing a file path object to the resource.
|
||||||
|
|
||||||
|
If the resource does not already exist on its own on the file system,
|
||||||
|
a temporary file will be created. If the file was created, the file
|
||||||
|
will be deleted upon exiting the context manager (no exception is
|
||||||
|
raised if the file was deleted prior to the context manager
|
||||||
|
exiting).
|
||||||
|
"""
|
||||||
|
return _common.as_file(_common.files(package) / normalize_path(resource))
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import abc
|
||||||
|
from typing import BinaryIO, Iterable, Text
|
||||||
|
|
||||||
|
from ._compat import runtime_checkable, Protocol
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceReader(metaclass=abc.ABCMeta):
|
||||||
|
"""Abstract base class for loaders to provide resource reading support."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def open_resource(self, resource: Text) -> BinaryIO:
|
||||||
|
"""Return an opened, file-like object for binary reading.
|
||||||
|
|
||||||
|
The 'resource' argument is expected to represent only a file name.
|
||||||
|
If the resource cannot be found, FileNotFoundError is raised.
|
||||||
|
"""
|
||||||
|
# This deliberately raises FileNotFoundError instead of
|
||||||
|
# NotImplementedError so that if this method is accidentally called,
|
||||||
|
# it'll still do the right thing.
|
||||||
|
raise FileNotFoundError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def resource_path(self, resource: Text) -> Text:
|
||||||
|
"""Return the file system path to the specified resource.
|
||||||
|
|
||||||
|
The 'resource' argument is expected to represent only a file name.
|
||||||
|
If the resource does not exist on the file system, raise
|
||||||
|
FileNotFoundError.
|
||||||
|
"""
|
||||||
|
# This deliberately raises FileNotFoundError instead of
|
||||||
|
# NotImplementedError so that if this method is accidentally called,
|
||||||
|
# it'll still do the right thing.
|
||||||
|
raise FileNotFoundError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_resource(self, path: Text) -> bool:
|
||||||
|
"""Return True if the named 'path' is a resource.
|
||||||
|
|
||||||
|
Files are resources, directories are not.
|
||||||
|
"""
|
||||||
|
raise FileNotFoundError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def contents(self) -> Iterable[str]:
|
||||||
|
"""Return an iterable of entries in `package`."""
|
||||||
|
raise FileNotFoundError
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class Traversable(Protocol):
|
||||||
|
"""
|
||||||
|
An object with a subset of pathlib.Path methods suitable for
|
||||||
|
traversing directories and opening files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def iterdir(self):
|
||||||
|
"""
|
||||||
|
Yield Traversable objects in self
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read_bytes(self):
|
||||||
|
"""
|
||||||
|
Read contents of self as bytes
|
||||||
|
"""
|
||||||
|
with self.open('rb') as strm:
|
||||||
|
return strm.read()
|
||||||
|
|
||||||
|
def read_text(self, encoding=None):
|
||||||
|
"""
|
||||||
|
Read contents of self as text
|
||||||
|
"""
|
||||||
|
with self.open(encoding=encoding) as strm:
|
||||||
|
return strm.read()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_dir(self) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if self is a directory
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_file(self) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if self is a file
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def joinpath(self, child):
|
||||||
|
"""
|
||||||
|
Return Traversable child in self
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __truediv__(self, child):
|
||||||
|
"""
|
||||||
|
Return Traversable child in self
|
||||||
|
"""
|
||||||
|
return self.joinpath(child)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def open(self, mode='r', *args, **kwargs):
|
||||||
|
"""
|
||||||
|
mode may be 'r' or 'rb' to open as text or binary. Return a handle
|
||||||
|
suitable for reading (same as pathlib.Path.open).
|
||||||
|
|
||||||
|
When opening as text, accepts encoding parameters such as those
|
||||||
|
accepted by io.TextIOWrapper.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def name(self) -> str:
|
||||||
|
"""
|
||||||
|
The base name of this object without any parent references.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TraversableResources(ResourceReader):
|
||||||
|
"""
|
||||||
|
The required interface for providing traversable
|
||||||
|
resources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def files(self):
|
||||||
|
"""Return a Traversable object for the loaded package."""
|
||||||
|
|
||||||
|
def open_resource(self, resource):
|
||||||
|
return self.files().joinpath(resource).open('rb')
|
||||||
|
|
||||||
|
def resource_path(self, resource):
|
||||||
|
raise FileNotFoundError(resource)
|
||||||
|
|
||||||
|
def is_resource(self, path):
|
||||||
|
return self.files().joinpath(path).is_file()
|
||||||
|
|
||||||
|
def contents(self):
|
||||||
|
return (item.name for item in self.files().iterdir())
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import collections
|
||||||
|
import pathlib
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from . import abc
|
||||||
|
|
||||||
|
from ._itertools import unique_everseen
|
||||||
|
from ._compat import ZipPath
|
||||||
|
|
||||||
|
|
||||||
|
def remove_duplicates(items):
|
||||||
|
return iter(collections.OrderedDict.fromkeys(items))
|
||||||
|
|
||||||
|
|
||||||
|
class FileReader(abc.TraversableResources):
|
||||||
|
def __init__(self, loader):
|
||||||
|
self.path = pathlib.Path(loader.path).parent
|
||||||
|
|
||||||
|
def resource_path(self, resource):
|
||||||
|
"""
|
||||||
|
Return the file system path to prevent
|
||||||
|
`resources.path()` from creating a temporary
|
||||||
|
copy.
|
||||||
|
"""
|
||||||
|
return str(self.path.joinpath(resource))
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
return self.path
|
||||||
|
|
||||||
|
|
||||||
|
class ZipReader(abc.TraversableResources):
|
||||||
|
def __init__(self, loader, module):
|
||||||
|
_, _, name = module.rpartition('.')
|
||||||
|
self.prefix = loader.prefix.replace('\\', '/') + name + '/'
|
||||||
|
self.archive = loader.archive
|
||||||
|
|
||||||
|
def open_resource(self, resource):
|
||||||
|
try:
|
||||||
|
return super().open_resource(resource)
|
||||||
|
except KeyError as exc:
|
||||||
|
raise FileNotFoundError(exc.args[0])
|
||||||
|
|
||||||
|
def is_resource(self, path):
|
||||||
|
# workaround for `zipfile.Path.is_file` returning true
|
||||||
|
# for non-existent paths.
|
||||||
|
target = self.files().joinpath(path)
|
||||||
|
return target.is_file() and target.exists()
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
return ZipPath(self.archive, self.prefix)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplexedPath(abc.Traversable):
|
||||||
|
"""
|
||||||
|
Given a series of Traversable objects, implement a merged
|
||||||
|
version of the interface across all objects. Useful for
|
||||||
|
namespace packages which may be multihomed at a single
|
||||||
|
name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *paths):
|
||||||
|
self._paths = list(map(pathlib.Path, remove_duplicates(paths)))
|
||||||
|
if not self._paths:
|
||||||
|
message = 'MultiplexedPath must contain at least one path'
|
||||||
|
raise FileNotFoundError(message)
|
||||||
|
if not all(path.is_dir() for path in self._paths):
|
||||||
|
raise NotADirectoryError('MultiplexedPath only supports directories')
|
||||||
|
|
||||||
|
def iterdir(self):
|
||||||
|
files = (file for path in self._paths for file in path.iterdir())
|
||||||
|
return unique_everseen(files, key=operator.attrgetter('name'))
|
||||||
|
|
||||||
|
def read_bytes(self):
|
||||||
|
raise FileNotFoundError(f'{self} is not a file')
|
||||||
|
|
||||||
|
def read_text(self, *args, **kwargs):
|
||||||
|
raise FileNotFoundError(f'{self} is not a file')
|
||||||
|
|
||||||
|
def is_dir(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_file(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def joinpath(self, child):
|
||||||
|
# first try to find child in current paths
|
||||||
|
for file in self.iterdir():
|
||||||
|
if file.name == child:
|
||||||
|
return file
|
||||||
|
# if it does not exist, construct it with the first path
|
||||||
|
return self._paths[0] / child
|
||||||
|
|
||||||
|
__truediv__ = joinpath
|
||||||
|
|
||||||
|
def open(self, *args, **kwargs):
|
||||||
|
raise FileNotFoundError(f'{self} is not a file')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._paths[0].name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
paths = ', '.join(f"'{path}'" for path in self._paths)
|
||||||
|
return f'MultiplexedPath({paths})'
|
||||||
|
|
||||||
|
|
||||||
|
class NamespaceReader(abc.TraversableResources):
|
||||||
|
def __init__(self, namespace_path):
|
||||||
|
if 'NamespacePath' not in str(namespace_path):
|
||||||
|
raise ValueError('Invalid path')
|
||||||
|
self.path = MultiplexedPath(*list(namespace_path))
|
||||||
|
|
||||||
|
def resource_path(self, resource):
|
||||||
|
"""
|
||||||
|
Return the file system path to prevent
|
||||||
|
`resources.path()` from creating a temporary
|
||||||
|
copy.
|
||||||
|
"""
|
||||||
|
return str(self.path.joinpath(resource))
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
return self.path
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Interface adapters for low-level readers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import io
|
||||||
|
import itertools
|
||||||
|
from typing import BinaryIO, List
|
||||||
|
|
||||||
|
from .abc import Traversable, TraversableResources
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleReader(abc.ABC):
|
||||||
|
"""
|
||||||
|
The minimum, low-level interface required from a resource
|
||||||
|
provider.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def package(self):
|
||||||
|
# type: () -> str
|
||||||
|
"""
|
||||||
|
The name of the package for which this reader loads resources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def children(self):
|
||||||
|
# type: () -> List['SimpleReader']
|
||||||
|
"""
|
||||||
|
Obtain an iterable of SimpleReader for available
|
||||||
|
child containers (e.g. directories).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def resources(self):
|
||||||
|
# type: () -> List[str]
|
||||||
|
"""
|
||||||
|
Obtain available named resources for this virtual package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def open_binary(self, resource):
|
||||||
|
# type: (str) -> BinaryIO
|
||||||
|
"""
|
||||||
|
Obtain a File-like for a named resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.package.split('.')[-1]
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceHandle(Traversable):
|
||||||
|
"""
|
||||||
|
Handle to a named resource in a ResourceReader.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent, name):
|
||||||
|
# type: (ResourceContainer, str) -> None
|
||||||
|
self.parent = parent
|
||||||
|
self.name = name # type: ignore
|
||||||
|
|
||||||
|
def is_file(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_dir(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def open(self, mode='r', *args, **kwargs):
|
||||||
|
stream = self.parent.reader.open_binary(self.name)
|
||||||
|
if 'b' not in mode:
|
||||||
|
stream = io.TextIOWrapper(*args, **kwargs)
|
||||||
|
return stream
|
||||||
|
|
||||||
|
def joinpath(self, name):
|
||||||
|
raise RuntimeError("Cannot traverse into a resource")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceContainer(Traversable):
|
||||||
|
"""
|
||||||
|
Traversable container for a package's resources via its reader.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, reader):
|
||||||
|
# type: (SimpleReader) -> None
|
||||||
|
self.reader = reader
|
||||||
|
|
||||||
|
def is_dir(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_file(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def iterdir(self):
|
||||||
|
files = (ResourceHandle(self, name) for name in self.reader.resources)
|
||||||
|
dirs = map(ResourceContainer, self.reader.children())
|
||||||
|
return itertools.chain(files, dirs)
|
||||||
|
|
||||||
|
def open(self, *args, **kwargs):
|
||||||
|
raise IsADirectoryError()
|
||||||
|
|
||||||
|
def joinpath(self, name):
|
||||||
|
return next(
|
||||||
|
traversable for traversable in self.iterdir() if traversable.name == name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TraversableReader(TraversableResources, SimpleReader):
|
||||||
|
"""
|
||||||
|
A TraversableResources based on SimpleReader. Resource providers
|
||||||
|
may derive from this class to provide the TraversableResources
|
||||||
|
interface by supplying the SimpleReader interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
return ResourceContainer(self)
|
||||||
Executable → Regular
+1
-1
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
Executable → Regular
+15
-12
@@ -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)"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
+110
-95
@@ -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:
|
||||||
else:
|
path (fsnative): The path to normalize
|
||||||
def _winpath2bytes(text, codec):
|
Returns:
|
||||||
if _codec_fails_on_encode_surrogates(codec):
|
`fsnative`
|
||||||
try:
|
|
||||||
return text.encode(codec)
|
Normalizes an fsnative path.
|
||||||
except UnicodeEncodeError:
|
|
||||||
return _winpath2bytes_py3(text, codec)
|
The same underlying path can have multiple representations as fsnative
|
||||||
else:
|
(due to surrogate pairs and variable length encodings). When concatenating
|
||||||
return _winpath2bytes_py3(text, codec)
|
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:
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
try:
|
if PY2:
|
||||||
return _winpath2bytes(path, encoding)
|
try:
|
||||||
except LookupError:
|
return path.encode(encoding)
|
||||||
raise ValueError("invalid encoding %r" % encoding)
|
except LookupError:
|
||||||
|
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
@@ -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
@@ -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
@@ -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
@@ -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
+168
-130
@@ -9,175 +9,213 @@
|
|||||||
# 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 = []
|
||||||
GetCommandLineW.restype = wintypes.LPCWSTR
|
GetCommandLineW.restype = wintypes.LPCWSTR
|
||||||
|
|
||||||
CommandLineToArgvW = shell32.CommandLineToArgvW
|
CommandLineToArgvW = shell32.CommandLineToArgvW
|
||||||
CommandLineToArgvW.argtypes = [
|
CommandLineToArgvW.argtypes = [
|
||||||
wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
|
wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
|
||||||
CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
|
CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
|
||||||
|
|
||||||
LocalFree = kernel32.LocalFree
|
LocalFree = kernel32.LocalFree
|
||||||
LocalFree.argtypes = [wintypes.HLOCAL]
|
LocalFree.argtypes = [wintypes.HLOCAL]
|
||||||
LocalFree.restype = wintypes.HLOCAL
|
LocalFree.restype = wintypes.HLOCAL
|
||||||
|
|
||||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751.aspx
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751.aspx
|
||||||
LPCTSTR = ctypes.c_wchar_p
|
LPCTSTR = ctypes.c_wchar_p
|
||||||
LPWSTR = wintypes.LPWSTR
|
LPWSTR = wintypes.LPWSTR
|
||||||
LPCWSTR = ctypes.c_wchar_p
|
LPCWSTR = ctypes.c_wchar_p
|
||||||
LPTSTR = LPWSTR
|
LPTSTR = LPWSTR
|
||||||
PCWSTR = ctypes.c_wchar_p
|
PCWSTR = ctypes.c_wchar_p
|
||||||
PCTSTR = PCWSTR
|
PCTSTR = PCWSTR
|
||||||
PWSTR = ctypes.c_wchar_p
|
PWSTR = ctypes.c_wchar_p
|
||||||
PTSTR = PWSTR
|
PTSTR = PWSTR
|
||||||
LPVOID = wintypes.LPVOID
|
LPVOID = wintypes.LPVOID
|
||||||
WCHAR = wintypes.WCHAR
|
WCHAR = wintypes.WCHAR
|
||||||
LPSTR = ctypes.c_char_p
|
LPSTR = ctypes.c_char_p
|
||||||
|
|
||||||
BOOL = wintypes.BOOL
|
BOOL = wintypes.BOOL
|
||||||
LPBOOL = ctypes.POINTER(BOOL)
|
LPBOOL = ctypes.POINTER(BOOL)
|
||||||
UINT = wintypes.UINT
|
UINT = wintypes.UINT
|
||||||
WORD = wintypes.WORD
|
WORD = wintypes.WORD
|
||||||
DWORD = wintypes.DWORD
|
DWORD = wintypes.DWORD
|
||||||
SHORT = wintypes.SHORT
|
SHORT = wintypes.SHORT
|
||||||
HANDLE = wintypes.HANDLE
|
HANDLE = wintypes.HANDLE
|
||||||
ULONG = wintypes.ULONG
|
ULONG = wintypes.ULONG
|
||||||
LPCSTR = wintypes.LPCSTR
|
LPCSTR = wintypes.LPCSTR
|
||||||
|
|
||||||
STD_INPUT_HANDLE = DWORD(-10)
|
STD_INPUT_HANDLE = DWORD(-10)
|
||||||
STD_OUTPUT_HANDLE = DWORD(-11)
|
STD_OUTPUT_HANDLE = DWORD(-11)
|
||||||
STD_ERROR_HANDLE = DWORD(-12)
|
STD_ERROR_HANDLE = DWORD(-12)
|
||||||
|
|
||||||
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
|
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
|
||||||
|
|
||||||
INTERNET_MAX_SCHEME_LENGTH = 32
|
INTERNET_MAX_SCHEME_LENGTH = 32
|
||||||
INTERNET_MAX_PATH_LENGTH = 2048
|
INTERNET_MAX_PATH_LENGTH = 2048
|
||||||
INTERNET_MAX_URL_LENGTH = (
|
INTERNET_MAX_URL_LENGTH = (
|
||||||
INTERNET_MAX_SCHEME_LENGTH + len("://") + INTERNET_MAX_PATH_LENGTH)
|
INTERNET_MAX_SCHEME_LENGTH + len("://") + INTERNET_MAX_PATH_LENGTH)
|
||||||
|
|
||||||
FOREGROUND_BLUE = 0x0001
|
FOREGROUND_BLUE = 0x0001
|
||||||
FOREGROUND_GREEN = 0x0002
|
FOREGROUND_GREEN = 0x0002
|
||||||
FOREGROUND_RED = 0x0004
|
FOREGROUND_RED = 0x0004
|
||||||
FOREGROUND_INTENSITY = 0x0008
|
FOREGROUND_INTENSITY = 0x0008
|
||||||
|
|
||||||
BACKGROUND_BLUE = 0x0010
|
BACKGROUND_BLUE = 0x0010
|
||||||
BACKGROUND_GREEN = 0x0020
|
BACKGROUND_GREEN = 0x0020
|
||||||
BACKGROUND_RED = 0x0040
|
BACKGROUND_RED = 0x0040
|
||||||
BACKGROUND_INTENSITY = 0x0080
|
BACKGROUND_INTENSITY = 0x0080
|
||||||
|
|
||||||
COMMON_LVB_REVERSE_VIDEO = 0x4000
|
COMMON_LVB_REVERSE_VIDEO = 0x4000
|
||||||
COMMON_LVB_UNDERSCORE = 0x8000
|
COMMON_LVB_UNDERSCORE = 0x8000
|
||||||
|
|
||||||
UrlCreateFromPathW = shlwapi.UrlCreateFromPathW
|
UrlCreateFromPathW = shlwapi.UrlCreateFromPathW
|
||||||
UrlCreateFromPathW.argtypes = [
|
UrlCreateFromPathW.argtypes = [
|
||||||
PCTSTR, PTSTR, ctypes.POINTER(DWORD), DWORD]
|
PCTSTR, PTSTR, ctypes.POINTER(DWORD), DWORD]
|
||||||
UrlCreateFromPathW.restype = ctypes.HRESULT
|
UrlCreateFromPathW.restype = ctypes.HRESULT
|
||||||
|
|
||||||
SetEnvironmentVariableW = kernel32.SetEnvironmentVariableW
|
SetEnvironmentVariableW = kernel32.SetEnvironmentVariableW
|
||||||
SetEnvironmentVariableW.argtypes = [LPCTSTR, LPCTSTR]
|
SetEnvironmentVariableW.argtypes = [LPCTSTR, LPCTSTR]
|
||||||
SetEnvironmentVariableW.restype = wintypes.BOOL
|
SetEnvironmentVariableW.restype = wintypes.BOOL
|
||||||
|
|
||||||
GetEnvironmentVariableW = kernel32.GetEnvironmentVariableW
|
GetEnvironmentVariableW = kernel32.GetEnvironmentVariableW
|
||||||
GetEnvironmentVariableW.argtypes = [LPCTSTR, LPTSTR, DWORD]
|
GetEnvironmentVariableW.argtypes = [LPCTSTR, LPTSTR, DWORD]
|
||||||
GetEnvironmentVariableW.restype = DWORD
|
GetEnvironmentVariableW.restype = DWORD
|
||||||
|
|
||||||
GetEnvironmentStringsW = kernel32.GetEnvironmentStringsW
|
GetEnvironmentStringsW = kernel32.GetEnvironmentStringsW
|
||||||
GetEnvironmentStringsW.argtypes = []
|
GetEnvironmentStringsW.argtypes = []
|
||||||
GetEnvironmentStringsW.restype = ctypes.c_void_p
|
GetEnvironmentStringsW.restype = ctypes.c_void_p
|
||||||
|
|
||||||
FreeEnvironmentStringsW = kernel32.FreeEnvironmentStringsW
|
FreeEnvironmentStringsW = kernel32.FreeEnvironmentStringsW
|
||||||
FreeEnvironmentStringsW.argtypes = [ctypes.c_void_p]
|
FreeEnvironmentStringsW.argtypes = [ctypes.c_void_p]
|
||||||
FreeEnvironmentStringsW.restype = ctypes.c_bool
|
FreeEnvironmentStringsW.restype = ctypes.c_bool
|
||||||
|
|
||||||
GetStdHandle = kernel32.GetStdHandle
|
GetStdHandle = kernel32.GetStdHandle
|
||||||
GetStdHandle.argtypes = [DWORD]
|
GetStdHandle.argtypes = [DWORD]
|
||||||
GetStdHandle.restype = HANDLE
|
GetStdHandle.restype = HANDLE
|
||||||
|
|
||||||
|
class COORD(ctypes.Structure):
|
||||||
|
|
||||||
class COORD(ctypes.Structure):
|
_fields_ = [
|
||||||
|
("X", SHORT),
|
||||||
|
("Y", SHORT),
|
||||||
|
]
|
||||||
|
|
||||||
_fields_ = [
|
class SMALL_RECT(ctypes.Structure):
|
||||||
("X", SHORT),
|
|
||||||
("Y", SHORT),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
("Left", SHORT),
|
||||||
|
("Top", SHORT),
|
||||||
|
("Right", SHORT),
|
||||||
|
("Bottom", SHORT),
|
||||||
|
]
|
||||||
|
|
||||||
class SMALL_RECT(ctypes.Structure):
|
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
|
||||||
|
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
("Left", SHORT),
|
("dwSize", COORD),
|
||||||
("Top", SHORT),
|
("dwCursorPosition", COORD),
|
||||||
("Right", SHORT),
|
("wAttributes", WORD),
|
||||||
("Bottom", SHORT),
|
("srWindow", SMALL_RECT),
|
||||||
]
|
("dwMaximumWindowSize", COORD),
|
||||||
|
]
|
||||||
|
|
||||||
|
GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo
|
||||||
|
GetConsoleScreenBufferInfo.argtypes = [
|
||||||
|
HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)]
|
||||||
|
GetConsoleScreenBufferInfo.restype = BOOL
|
||||||
|
|
||||||
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
|
GetConsoleOutputCP = kernel32.GetConsoleOutputCP
|
||||||
|
GetConsoleOutputCP.argtypes = []
|
||||||
|
GetConsoleOutputCP.restype = UINT
|
||||||
|
|
||||||
_fields_ = [
|
SetConsoleOutputCP = kernel32.SetConsoleOutputCP
|
||||||
("dwSize", COORD),
|
SetConsoleOutputCP.argtypes = [UINT]
|
||||||
("dwCursorPosition", COORD),
|
SetConsoleOutputCP.restype = BOOL
|
||||||
("wAttributes", WORD),
|
|
||||||
("srWindow", SMALL_RECT),
|
|
||||||
("dwMaximumWindowSize", COORD),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
GetConsoleCP = kernel32.GetConsoleCP
|
||||||
|
GetConsoleCP.argtypes = []
|
||||||
|
GetConsoleCP.restype = UINT
|
||||||
|
|
||||||
GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo
|
SetConsoleCP = kernel32.SetConsoleCP
|
||||||
GetConsoleScreenBufferInfo.argtypes = [
|
SetConsoleCP.argtypes = [UINT]
|
||||||
HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)]
|
SetConsoleCP.restype = BOOL
|
||||||
GetConsoleScreenBufferInfo.restype = BOOL
|
|
||||||
|
|
||||||
GetConsoleOutputCP = kernel32.GetConsoleOutputCP
|
SetConsoleTextAttribute = kernel32.SetConsoleTextAttribute
|
||||||
GetConsoleOutputCP.argtypes = []
|
SetConsoleTextAttribute.argtypes = [HANDLE, WORD]
|
||||||
GetConsoleOutputCP.restype = UINT
|
SetConsoleTextAttribute.restype = BOOL
|
||||||
|
|
||||||
SetConsoleOutputCP = kernel32.SetConsoleOutputCP
|
SetConsoleCursorPosition = kernel32.SetConsoleCursorPosition
|
||||||
SetConsoleOutputCP.argtypes = [UINT]
|
SetConsoleCursorPosition.argtypes = [HANDLE, COORD]
|
||||||
SetConsoleOutputCP.restype = BOOL
|
SetConsoleCursorPosition.restype = BOOL
|
||||||
|
|
||||||
GetConsoleCP = kernel32.GetConsoleCP
|
ReadConsoleW = kernel32.ReadConsoleW
|
||||||
GetConsoleCP.argtypes = []
|
ReadConsoleW.argtypes = [
|
||||||
GetConsoleCP.restype = UINT
|
HANDLE, LPVOID, DWORD, ctypes.POINTER(DWORD), LPVOID]
|
||||||
|
ReadConsoleW.restype = BOOL
|
||||||
|
|
||||||
SetConsoleCP = kernel32.SetConsoleCP
|
MultiByteToWideChar = kernel32.MultiByteToWideChar
|
||||||
SetConsoleCP.argtypes = [UINT]
|
MultiByteToWideChar.argtypes = [
|
||||||
SetConsoleCP.restype = BOOL
|
UINT, DWORD, LPCSTR, ctypes.c_int, LPWSTR, ctypes.c_int]
|
||||||
|
MultiByteToWideChar.restype = ctypes.c_int
|
||||||
|
|
||||||
SetConsoleTextAttribute = kernel32.SetConsoleTextAttribute
|
WideCharToMultiByte = kernel32.WideCharToMultiByte
|
||||||
SetConsoleTextAttribute.argtypes = [HANDLE, WORD]
|
WideCharToMultiByte.argtypes = [
|
||||||
SetConsoleTextAttribute.restype = BOOL
|
UINT, DWORD, LPCWSTR, ctypes.c_int, LPSTR, ctypes.c_int,
|
||||||
|
LPCSTR, LPBOOL]
|
||||||
|
WideCharToMultiByte.restype = ctypes.c_int
|
||||||
|
|
||||||
SetConsoleCursorPosition = kernel32.SetConsoleCursorPosition
|
MoveFileW = kernel32.MoveFileW
|
||||||
SetConsoleCursorPosition.argtypes = [HANDLE, COORD]
|
MoveFileW.argtypes = [LPCTSTR, LPCTSTR]
|
||||||
SetConsoleCursorPosition.restype = BOOL
|
MoveFileW.restype = BOOL
|
||||||
|
|
||||||
ReadConsoleW = kernel32.ReadConsoleW
|
GetFileInformationByHandleEx = None
|
||||||
ReadConsoleW.argtypes = [HANDLE, LPVOID, DWORD, ctypes.POINTER(DWORD), LPVOID]
|
if hasattr(kernel32, "GetFileInformationByHandleEx"):
|
||||||
ReadConsoleW.restype = BOOL
|
GetFileInformationByHandleEx = kernel32.GetFileInformationByHandleEx
|
||||||
|
GetFileInformationByHandleEx.argtypes = [
|
||||||
|
HANDLE, ctypes.c_int, ctypes.c_void_p, DWORD]
|
||||||
|
GetFileInformationByHandleEx.restype = BOOL
|
||||||
|
else:
|
||||||
|
# Windows XP
|
||||||
|
pass
|
||||||
|
|
||||||
MultiByteToWideChar = kernel32.MultiByteToWideChar
|
MAX_PATH = 260
|
||||||
MultiByteToWideChar.argtypes = [
|
FileNameInfo = 2
|
||||||
UINT, DWORD, LPCSTR, ctypes.c_int, LPWSTR, ctypes.c_int]
|
|
||||||
MultiByteToWideChar.restype = ctypes.c_int
|
|
||||||
|
|
||||||
WideCharToMultiByte = kernel32.WideCharToMultiByte
|
class FILE_NAME_INFO(ctypes.Structure):
|
||||||
WideCharToMultiByte.argtypes = [
|
_fields_ = [
|
||||||
UINT, DWORD, LPCWSTR, ctypes.c_int, LPSTR, ctypes.c_int, LPCSTR, LPBOOL]
|
("FileNameLength", DWORD),
|
||||||
WideCharToMultiByte.restpye = ctypes.c_int
|
("FileName", WCHAR),
|
||||||
|
]
|
||||||
|
|
||||||
MoveFileW = kernel32.MoveFileW
|
_get_osfhandle = msvcrt._get_osfhandle
|
||||||
MoveFileW.argtypes = [LPCTSTR, LPCTSTR]
|
_get_osfhandle.argtypes = [ctypes.c_int]
|
||||||
MoveFileW.restype = BOOL
|
_get_osfhandle.restype = HANDLE
|
||||||
|
|
||||||
|
GetFileType = kernel32.GetFileType
|
||||||
|
GetFileType.argtypes = [HANDLE]
|
||||||
|
GetFileType.restype = DWORD
|
||||||
|
|
||||||
|
FILE_TYPE_PIPE = 0x0003
|
||||||
|
|||||||
Executable → Regular
+2
-2
@@ -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
Executable → Regular
+2
-2
@@ -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
@@ -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
@@ -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
+71
-48
@@ -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").decode("ascii")
|
||||||
return text.encode("ascii")
|
|
||||||
else:
|
|
||||||
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
+21
-21
@@ -46,28 +46,28 @@ 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
|
||||||
else:
|
|
||||||
m3u = None
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
page = OggPage(fileobj)
|
|
||||||
except EOFError:
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
format["stream"] = page.serial
|
m3u = None
|
||||||
if page.serial not in fileobjs:
|
while True:
|
||||||
new_filename = options.pattern % format
|
try:
|
||||||
new_fileobj = open(new_filename, "wb")
|
page = OggPage(fileobj)
|
||||||
fileobjs[page.serial] = new_fileobj
|
except EOFError:
|
||||||
if m3u:
|
break
|
||||||
m3u.write(new_filename + "\r\n")
|
else:
|
||||||
fileobjs[page.serial].write(page.write())
|
format["stream"] = page.serial
|
||||||
for f in list(fileobjs.values()):
|
if page.serial not in fileobjs:
|
||||||
f.close()
|
new_filename = options.pattern % format
|
||||||
|
new_fileobj = open(new_filename, "wb")
|
||||||
|
fileobjs[page.serial] = new_fileobj
|
||||||
|
if m3u:
|
||||||
|
m3u.write(new_filename + "\r\n")
|
||||||
|
fileobjs[page.serial].write(page.write())
|
||||||
|
for f in fileobjs.values():
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
def entry_point():
|
def entry_point():
|
||||||
|
|||||||
Executable → Regular
+5
-6
@@ -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():
|
||||||
|
|||||||
Executable → Regular
+2
-2
@@ -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
@@ -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
+16
-35
@@ -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,9 +124,7 @@ 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
|
tag = tag.decode("ascii")
|
||||||
if PY3:
|
|
||||||
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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
self.sample_rate = int(read_float(sample_rate))
|
try:
|
||||||
self.sample_size = sample_size
|
self.sample_rate = int(read_float(sample_rate))
|
||||||
|
except OverflowError:
|
||||||
|
raise error("Invalid 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.bits_per_sample = sample_size
|
||||||
|
self.sample_size = sample_size # For backward compatibility
|
||||||
self.channels = channels
|
self.channels = channels
|
||||||
self.bitrate = channels * sample_size * self.sample_rate
|
self.bitrate = channels * sample_size * self.sample_rate
|
||||||
self.length = frame_count / float(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
+40
-78
@@ -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,11 +323,10 @@ 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:
|
reraise(APEBadItemError, err, sys.exc_info()[2])
|
||||||
reraise(APEBadItemError, err, sys.exc_info()[2])
|
|
||||||
value = fileobj.read(size)
|
value = fileobj.read(size)
|
||||||
if len(value) != size:
|
if len(value) != size:
|
||||||
raise APEBadItemError
|
raise APEBadItemError
|
||||||
@@ -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
@@ -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
+14
-25
@@ -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,11 +120,8 @@ 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:
|
raise TypeError("%r not str" % value)
|
||||||
return value.decode("utf-8")
|
|
||||||
else:
|
|
||||||
raise TypeError("%r not str" % value)
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def _render(self):
|
def _render(self):
|
||||||
@@ -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
+24
-17
@@ -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")
|
||||||
|
|
||||||
obj.parse(asf, data)
|
try:
|
||||||
|
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
@@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
+35
-38
@@ -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
+20
-24
@@ -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
@@ -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
@@ -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
+31
-16
@@ -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,13 +151,17 @@ 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():
|
||||||
self.add(v)
|
if len(self.getall(v.HashKey)) == 0:
|
||||||
|
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..
|
||||||
if known_frames is not None:
|
if known_frames is not None:
|
||||||
@@ -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
@@ -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
@@ -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
+29
-38
@@ -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
+42
-50
@@ -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,11 +129,10 @@ 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:
|
continue
|
||||||
continue
|
|
||||||
if name in frames:
|
if name in frames:
|
||||||
asbpi += 1
|
asbpi += 1
|
||||||
else:
|
else:
|
||||||
@@ -151,11 +148,10 @@ 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:
|
continue
|
||||||
continue
|
|
||||||
if name in frames:
|
if name in frames:
|
||||||
asint += 1
|
asint += 1
|
||||||
else:
|
else:
|
||||||
@@ -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,8 +528,7 @@ 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)
|
||||||
return header + framedata
|
return header + framedata
|
||||||
@@ -575,11 +569,10 @@ 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:
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# someone writes 2.3 frames with 2.2 names
|
# someone writes 2.3 frames with 2.2 names
|
||||||
@@ -614,11 +607,10 @@ 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:
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tag = frames[name]
|
tag = frames[name]
|
||||||
|
|||||||
Executable → Regular
+3
-13
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+197
-36
@@ -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])
|
||||||
else:
|
|
||||||
self._padding = self.tags._padding
|
if not MP4Chapters._can_load(atoms):
|
||||||
|
self.chapters = None
|
||||||
|
else:
|
||||||
|
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
+16
-12
@@ -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
+2
-7
@@ -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,12 +179,8 @@ class Atoms(object):
|
|||||||
specifying the complete path ('moov.udta').
|
specifying the complete path ('moov.udta').
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if PY2:
|
if isinstance(names, bytes):
|
||||||
if isinstance(names, str):
|
names = names.split(b".")
|
||||||
names = names.split(b".")
|
|
||||||
else:
|
|
||||||
if isinstance(names, bytes):
|
|
||||||
names = names.split(b".")
|
|
||||||
|
|
||||||
for child in self.atoms:
|
for child in self.atoms:
|
||||||
if child.name == names[0]:
|
if child.name == names[0]:
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
+15
-8
@@ -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
+36
-24
@@ -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,11 +268,12 @@ class OggPage(object):
|
|||||||
else:
|
else:
|
||||||
sequence += 1
|
sequence += 1
|
||||||
|
|
||||||
if page.continued:
|
if page.packets:
|
||||||
packets[-1].append(page.packets[0])
|
if page.continued:
|
||||||
else:
|
packets[-1].append(page.packets[0])
|
||||||
packets.append([page.packets[0]])
|
else:
|
||||||
packets.extend([p] for p in page.packets[1:])
|
packets.append([page.packets[0]])
|
||||||
|
packets.extend([p] for p in page.packets[1:])
|
||||||
|
|
||||||
return [b"".join(p) for p in packets]
|
return [b"".join(p) for p in packets]
|
||||||
|
|
||||||
@@ -388,7 +390,7 @@ class OggPage(object):
|
|||||||
# Number the new pages starting from the first old page.
|
# Number the new pages starting from the first old page.
|
||||||
first = old_pages[0].sequence
|
first = old_pages[0].sequence
|
||||||
for page, seq in zip(new_pages,
|
for page, seq in zip(new_pages,
|
||||||
range(first, first + len(new_pages))):
|
range(first, first + len(new_pages))):
|
||||||
page.sequence = seq
|
page.sequence = seq
|
||||||
page.serial = old_pages[0].serial
|
page.serial = old_pages[0].serial
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
if page.serial == serial:
|
||||||
|
if is_valid(page):
|
||||||
|
best_page = page
|
||||||
|
if page.last:
|
||||||
|
break
|
||||||
page = OggPage(fileobj)
|
page = OggPage(fileobj)
|
||||||
while page.serial != serial:
|
return best_page
|
||||||
page = OggPage(fileobj)
|
|
||||||
best_page = page
|
|
||||||
return 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user