1# -*- coding: utf-8 -*-
2#
3# Picard, the next-generation MusicBrainz tagger
4#
5# Copyright (C) 2019-2020 Philipp Wolfer
6#
7# This program is free software; you can redistribute it and/or
8# modify it under the terms of the GNU General Public License
9# as published by the Free Software Foundation; either version 2
10# of the License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
21
22import unittest
23
24import mutagen
25
26from picard.formats import ext_to_format
27from picard.metadata import Metadata
28
29from .common import (
30    CommonTests,
31    load_metadata,
32    load_raw,
33    save_and_load_metadata,
34    save_metadata,
35    save_raw,
36    skipUnlessTestfile,
37)
38from .coverart import CommonCoverArtTests
39
40
41# prevent unittest to run tests in those classes
42class CommonMP4Tests:
43
44    class MP4TestCase(CommonTests.TagFormatsTestCase):
45        def test_supports_tag(self):
46            fmt = ext_to_format(self.testfile_ext[1:])
47            self.assertTrue(fmt.supports_tag('copyright'))
48            self.assertTrue(fmt.supports_tag('compilation'))
49            self.assertTrue(fmt.supports_tag('bpm'))
50            self.assertTrue(fmt.supports_tag('djmixer'))
51            self.assertTrue(fmt.supports_tag('discnumber'))
52            self.assertTrue(fmt.supports_tag('lyrics:lead'))
53            self.assertTrue(fmt.supports_tag('~length'))
54            self.assertTrue(fmt.supports_tag('Custom'))
55            self.assertTrue(fmt.supports_tag('äöüéß\0'))  # Latin 1 is supported
56            self.assertFalse(fmt.supports_tag('Б'))  # Unsupported custom tags
57            for tag in self.replaygain_tags.keys():
58                self.assertTrue(fmt.supports_tag(tag))
59
60        def test_format(self):
61            metadata = load_metadata(self.filename)
62            self.assertIn('AAC LC', metadata['~format'])
63
64        @skipUnlessTestfile
65        def test_replaygain_tags_case_insensitive(self):
66            tags = mutagen.mp4.MP4Tags()
67            tags['----:com.apple.iTunes:replaygain_album_gain'] = [b'-6.48 dB']
68            tags['----:com.apple.iTunes:Replaygain_Album_Peak'] = [b'0.978475']
69            tags['----:com.apple.iTunes:replaygain_album_range'] = [b'7.84 dB']
70            tags['----:com.apple.iTunes:replaygain_track_gain'] = [b'-6.16 dB']
71            tags['----:com.apple.iTunes:REPLAYGAIN_track_peak'] = [b'0.976991']
72            tags['----:com.apple.iTunes:REPLAYGAIN_TRACK_RANGE'] = [b'8.22 dB']
73            tags['----:com.apple.iTunes:replaygain_reference_loudness'] = [b'-18.00 LUFS']
74            save_raw(self.filename, tags)
75            loaded_metadata = load_metadata(self.filename)
76            for (key, value) in self.replaygain_tags.items():
77                self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))
78
79        @skipUnlessTestfile
80        def test_ci_tags_preserve_case(self):
81            # Ensure values are not duplicated on repeated save and are saved
82            # case preserving.
83            for name in ('Replaygain_Album_Peak', 'Custom', 'äöüéß\0'):
84                tags = mutagen.mp4.MP4Tags()
85                tags['----:com.apple.iTunes:' + name] = [b'foo']
86                save_raw(self.filename, tags)
87                loaded_metadata = load_metadata(self.filename)
88                loaded_metadata[name.lower()] = 'bar'
89                save_metadata(self.filename, loaded_metadata)
90                raw_metadata = load_raw(self.filename)
91                self.assertIn('----:com.apple.iTunes:' + name, raw_metadata)
92                self.assertEqual(
93                    raw_metadata['----:com.apple.iTunes:' + name][0].decode('utf-8'),
94                    loaded_metadata[name.lower()])
95                self.assertEqual(1, len(raw_metadata['----:com.apple.iTunes:' + name]))
96                self.assertNotIn('----:com.apple.iTunes:' + name.upper(), raw_metadata)
97
98        @skipUnlessTestfile
99        def test_delete_freeform_tags(self):
100            metadata = Metadata()
101            metadata['foo'] = 'bar'
102            original_metadata = save_and_load_metadata(self.filename, metadata)
103            self.assertEqual('bar', original_metadata['foo'])
104            del metadata['foo']
105            new_metadata = save_and_load_metadata(self.filename, metadata)
106            self.assertNotIn('foo', new_metadata)
107
108        @skipUnlessTestfile
109        def test_invalid_track_and_discnumber(self):
110            metadata = Metadata({
111                'discnumber': 'notanumber',
112                'tracknumber': 'notanumber',
113            })
114            loaded_metadata = save_and_load_metadata(self.filename, metadata)
115            self.assertNotIn('discnumber', loaded_metadata)
116            self.assertNotIn('tracknumber', loaded_metadata)
117
118        @skipUnlessTestfile
119        def test_invalid_total_tracks_and_discs(self):
120            metadata = Metadata({
121                'discnumber': '1',
122                'totaldiscs': 'notanumber',
123                'tracknumber': '2',
124                'totaltracks': 'notanumber',
125            })
126            loaded_metadata = save_and_load_metadata(self.filename, metadata)
127            self.assertEqual(metadata['discnumber'], loaded_metadata['discnumber'])
128            self.assertEqual('0', loaded_metadata['totaldiscs'])
129            self.assertEqual(metadata['tracknumber'], loaded_metadata['tracknumber'])
130            self.assertEqual('0', loaded_metadata['totaltracks'])
131
132        @skipUnlessTestfile
133        def test_invalid_int_tag(self):
134            for tag in ['bpm', 'movementnumber', 'movementtotal', 'showmovement']:
135                metadata = Metadata({tag: 'notanumber'})
136                loaded_metadata = save_and_load_metadata(self.filename, metadata)
137                self.assertNotIn(tag, loaded_metadata)
138
139
140class M4ATest(CommonMP4Tests.MP4TestCase):
141    testfile = 'test.m4a'
142    supports_ratings = False
143    expected_info = {
144        'length': 106,
145        '~channels': '2',
146        '~sample_rate': '44100',
147        '~bitrate': '14.376',
148        '~bits_per_sample': '16',
149    }
150    unexpected_info = ['~video']
151
152    @unittest.skipUnless(mutagen.version >= (1, 43, 0), "mutagen >= 1.43.0 required")
153    def test_hdvd_tag_considered_video(self):
154        tags = mutagen.mp4.MP4Tags()
155        tags['hdvd'] = [1]
156        save_raw(self.filename, tags)
157        metadata = load_metadata(self.filename)
158        self.assertEqual('1', metadata["~video"])
159
160
161class M4VTest(CommonMP4Tests.MP4TestCase):
162    testfile = 'test.m4v'
163    supports_ratings = False
164    expected_info = {
165        'length': 106,
166        '~channels': '2',
167        '~sample_rate': '44100',
168        '~bitrate': '108.043',
169        '~bits_per_sample': '16',
170        '~video': '1',
171    }
172
173
174class Mp4CoverArtTest(CommonCoverArtTests.CoverArtTestCase):
175    testfile = 'test.m4a'
176