# -*- coding: utf-8 -*- # # Picard, the next-generation MusicBrainz tagger # # 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. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os from mutagen.apev2 import ( BINARY, APEValue, ) from test.picardtestcase import PicardTestCase from picard import config from picard.formats import ( apev2, open_, ) from picard.formats.mutagenext.tak import native_tak from picard.metadata import Metadata from .common import ( TAGS, CommonTests, load_metadata, load_raw, save_metadata, save_raw, skipUnlessTestfile, ) from .coverart import CommonCoverArtTests VALID_KEYS = [ ' valid Key}', '{ $ome tag~}', 'xx', 'x' * 255, ] INVALID_KEYS = [ 'invalid\x7fkey', 'invalid\x19key', '', 'x', 'x' * 256, 'ID3', 'TAG', 'OggS', 'MP+', ] SUPPORTED_TAGS = list(set(TAGS.keys()) - set(apev2.UNSUPPORTED_TAGS)) class CommonApeTests: class ApeTestCase(CommonTests.TagFormatsTestCase): def setup_tags(self): super().setup_tags() self.unsupported_tags['r128_album_gain'] = '-2857' self.unsupported_tags['r128_track_gain'] = '-2857' def test_supports_tags(self): supports_tag = self.format.supports_tag for key in VALID_KEYS + SUPPORTED_TAGS: self.assertTrue(supports_tag(key), '%r should be supported' % key) for key in INVALID_KEYS + apev2.UNSUPPORTED_TAGS: self.assertFalse(supports_tag(key), '%r should be unsupported' % key) @skipUnlessTestfile def test_invalid_coverart(self): metadata = { 'Cover Art (Front)': APEValue(b'filename.png\0NOTPNGDATA', BINARY) } save_raw(self.filename, metadata) loaded_metadata = load_metadata(self.filename) self.assertEqual(0, len(loaded_metadata.images)) def test_supports_extended_tags(self): performer_tag = "performer:accordéon clavier « boutons »" self.assertTrue(self.format.supports_tag(performer_tag)) self.assertTrue(self.format.supports_tag('lyrics:foó')) self.assertTrue(self.format.supports_tag('comment:foó')) def test_case_insensitive_reading(self): self._read_case_insensitive_tag('artist', 'Artist') self._read_case_insensitive_tag('albumartist', 'Album Artist') self._read_case_insensitive_tag('performer:', 'Performer') self._read_case_insensitive_tag('tracknumber', 'Track') self._read_case_insensitive_tag('discnumber', 'Disc') @skipUnlessTestfile def test_ci_tags_preserve_case(self): # Ensure values are not duplicated on repeated save and are saved # case preserving. for name in ('CUStom', 'ARtist'): tags = {} tags[name] = 'foo' save_raw(self.filename, tags) loaded_metadata = load_metadata(self.filename) loaded_metadata[name.lower()] = 'bar' save_metadata(self.filename, loaded_metadata) raw_metadata = dict(load_raw(self.filename)) self.assertIn(name, raw_metadata) self.assertEqual( raw_metadata[name], loaded_metadata[name.lower()]) self.assertEqual(1, len(raw_metadata[name])) self.assertNotIn(name.upper(), raw_metadata) def _read_case_insensitive_tag(self, name, ape_name): upper_ape_name = ape_name.upper() metadata = { upper_ape_name: 'Some value' } save_raw(self.filename, metadata) loaded_metadata = load_metadata(self.filename) self.assertEqual(metadata[upper_ape_name], loaded_metadata[name]) save_metadata(self.filename, loaded_metadata) raw_metadata = load_raw(self.filename) self.assertIn(upper_ape_name, raw_metadata.keys()) self.assertEqual(metadata[upper_ape_name], raw_metadata[ape_name]) class MonkeysAudioTest(CommonApeTests.ApeTestCase): testfile = 'test.ape' supports_ratings = False expected_info = { 'length': 82, '~channels': '2', '~sample_rate': '44100', '~bits_per_sample': '16', } unexpected_info = ['~video'] class WavPackTest(CommonApeTests.ApeTestCase): testfile = 'test.wv' supports_ratings = False expected_info = { 'length': 82, '~channels': '2', '~sample_rate': '44100', } unexpected_info = ['~video'] @skipUnlessTestfile def test_save_wavpack_correction_file(self): config.setting['rename_files'] = True config.setting['move_files'] = False config.setting['ascii_filenames'] = False config.setting['windows_compatibility'] = False config.setting['dont_write_tags'] = True config.setting['preserve_timestamps'] = False config.setting['delete_empty_dirs'] = False config.setting['save_images_to_files'] = False config.setting['file_naming_format'] = '%title%' # Create dummy WavPack correction file source_file_wvc = self.filename + 'c' open(source_file_wvc, 'a').close() # Open file and rename it f = open_(self.filename) metadata = Metadata({'title': 'renamed_' + os.path.basename(self.filename)}) self.assertTrue(os.path.isfile(self.filename)) target_file_wv = f._save_and_rename(self.filename, metadata) target_file_wvc = target_file_wv + 'c' # Register cleanups self.addCleanup(os.unlink, target_file_wv) self.addCleanup(os.unlink, target_file_wvc) # Check both the WavPack file and the correction file got moved self.assertFalse(os.path.isfile(self.filename)) self.assertFalse(os.path.isfile(source_file_wvc)) self.assertTrue(os.path.isfile(target_file_wv)) self.assertTrue(os.path.isfile(target_file_wvc)) class MusepackSV7Test(CommonApeTests.ApeTestCase): testfile = 'test-sv7.mpc' supports_ratings = False expected_info = { 'length': 91, '~channels': '2', '~sample_rate': '44100', } unexpected_info = ['~video'] class MusepackSV8Test(CommonApeTests.ApeTestCase): testfile = 'test-sv8.mpc' supports_ratings = False expected_info = { 'length': 82, '~channels': '2', '~sample_rate': '44100', } unexpected_info = ['~video'] class TAKTest(CommonApeTests.ApeTestCase): testfile = 'test.tak' supports_ratings = False unexpected_info = ['~video'] def setUp(self): super().setUp() if native_tak: self.expected_info = { 'length': 82, '~channels': '2', '~sample_rate': '44100', '~bits_per_sample': '16' } class OptimFROGLosslessTest(CommonApeTests.ApeTestCase): testfile = 'test.ofr' supports_ratings = False expected_info = { 'length': 0, '~channels': '2', '~sample_rate': '48000', } unexpected_info = ['~video'] def test_format(self): metadata = load_metadata(self.filename) self.assertEqual(metadata['~format'], 'OptimFROG Lossless Audio') class OptimFROGDUalStreamTest(CommonApeTests.ApeTestCase): testfile = 'test.ofs' supports_ratings = False expected_info = { 'length': 0, '~channels': '2', '~sample_rate': '48000', } unexpected_info = ['~video'] def test_format(self): metadata = load_metadata(self.filename) self.assertEqual(metadata['~format'], 'OptimFROG DualStream Audio') class ApeCoverArtTest(CommonCoverArtTests.CoverArtTestCase): testfile = 'test.ape' supports_types = False class Apev2UtilTest(PicardTestCase): def test_is_valid_key(self): for key in VALID_KEYS: self.assertTrue(apev2.is_valid_key(key), '%r is valid' % key) for key in INVALID_KEYS: self.assertFalse(apev2.is_valid_key(key), '%r is invalid' % key)