1# -*- coding: utf-8 -*-
2#
3# Picard, the next-generation MusicBrainz tagger
4#
5# Copyright (C) 2006-2008, 2011 Lukáš Lalinský
6# Copyright (C) 2008-2009 Nikolai Prokoschenko
7# Copyright (C) 2009-2010, 2014-2015, 2018-2021 Philipp Wolfer
8# Copyright (C) 2011-2013 Michael Wiencek
9# Copyright (C) 2011-2013 Wieland Hoffmann
10# Copyright (C) 2013 Calvin Walton
11# Copyright (C) 2013 Ionuț Ciocîrlan
12# Copyright (C) 2013-2014 Sophist-UK
13# Copyright (C) 2013-2015, 2018-2019 Laurent Monin
14# Copyright (C) 2015 Alex Berman
15# Copyright (C) 2015 Ohm Patel
16# Copyright (C) 2016 Suhas
17# Copyright (C) 2016-2017 Sambhav Kothari
18#
19# This program is free software; you can redistribute it and/or
20# modify it under the terms of the GNU General Public License
21# as published by the Free Software Foundation; either version 2
22# of the License, or (at your option) any later version.
23#
24# This program is distributed in the hope that it will be useful,
25# but WITHOUT ANY WARRANTY; without even the implied warranty of
26# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27# GNU General Public License for more details.
28#
29# You should have received a copy of the GNU General Public License
30# along with this program; if not, write to the Free Software
31# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
32
33
34from functools import partial
35import os.path
36
37from PyQt5 import QtWidgets
38from PyQt5.QtCore import QStandardPaths
39from PyQt5.QtGui import QPalette
40
41from picard.config import (
42    BoolOption,
43    TextOption,
44    get_config,
45)
46from picard.const import DEFAULT_FILE_NAMING_FORMAT
47from picard.const.sys import IS_WIN
48from picard.file import File
49from picard.script import (
50    ScriptError,
51    ScriptParser,
52)
53from picard.util.settingsoverride import SettingsOverride
54
55from picard.ui.options import (
56    OptionsCheckError,
57    OptionsPage,
58    register_options_page,
59)
60from picard.ui.options.scripting import (
61    ScriptCheckError,
62    ScriptingDocumentationDialog,
63)
64from picard.ui.ui_options_renaming import Ui_RenamingOptionsPage
65from picard.ui.util import enabledSlot
66
67
68_default_music_dir = QStandardPaths.writableLocation(QStandardPaths.MusicLocation)
69
70
71class RenamingOptionsPage(OptionsPage):
72
73    NAME = "filerenaming"
74    TITLE = N_("File Naming")
75    PARENT = None
76    SORT_ORDER = 40
77    ACTIVE = True
78    HELP_URL = '/config/options_filerenaming.html'
79
80    options = [
81        BoolOption("setting", "windows_compatibility", True),
82        BoolOption("setting", "ascii_filenames", False),
83        BoolOption("setting", "rename_files", False),
84        TextOption(
85            "setting",
86            "file_naming_format",
87            DEFAULT_FILE_NAMING_FORMAT,
88        ),
89        BoolOption("setting", "move_files", False),
90        TextOption("setting", "move_files_to", _default_music_dir),
91        BoolOption("setting", "move_additional_files", False),
92        TextOption("setting", "move_additional_files_pattern", "*.jpg *.png"),
93        BoolOption("setting", "delete_empty_dirs", True),
94    ]
95
96    def __init__(self, parent=None):
97        super().__init__(parent)
98        self.ui = Ui_RenamingOptionsPage()
99        self.ui.setupUi(self)
100
101        self.ui.ascii_filenames.clicked.connect(self.update_examples)
102        self.ui.windows_compatibility.clicked.connect(self.update_examples)
103        self.ui.rename_files.clicked.connect(self.update_examples)
104        self.ui.move_files.clicked.connect(self.update_examples)
105        self.ui.move_files_to.editingFinished.connect(self.update_examples)
106
107        self.ui.move_files.toggled.connect(
108            partial(
109                enabledSlot,
110                self.toggle_file_moving
111            )
112        )
113        self.ui.rename_files.toggled.connect(
114            partial(
115                enabledSlot,
116                self.toggle_file_renaming
117            )
118        )
119        self.ui.file_naming_format.textChanged.connect(self.check_formats)
120        self.ui.file_naming_format_default.clicked.connect(self.set_file_naming_format_default)
121        self.ui.move_files_to_browse.clicked.connect(self.move_files_to_browse)
122
123        script_edit = self.ui.file_naming_format
124        self.script_palette_normal = script_edit.palette()
125        self.script_palette_readonly = QPalette(self.script_palette_normal)
126        disabled_color = self.script_palette_normal.color(QPalette.Inactive, QPalette.Window)
127        self.script_palette_readonly.setColor(QPalette.Disabled, QPalette.Base, disabled_color)
128        self.ui.scripting_documentation_button.clicked.connect(self.show_scripting_documentation)
129
130    def show_scripting_documentation(self):
131        ScriptingDocumentationDialog.show_instance(parent=self)
132
133    def toggle_file_moving(self, state):
134        self.toggle_file_naming_format()
135        self.ui.delete_empty_dirs.setEnabled(state)
136        self.ui.move_files_to.setEnabled(state)
137        self.ui.move_files_to_browse.setEnabled(state)
138        self.ui.move_additional_files.setEnabled(state)
139        self.ui.move_additional_files_pattern.setEnabled(state)
140
141    def toggle_file_renaming(self, state):
142        self.toggle_file_naming_format()
143
144    def toggle_file_naming_format(self):
145        active = self.ui.move_files.isChecked() or self.ui.rename_files.isChecked()
146        self.ui.file_naming_format.setEnabled(active)
147        self.ui.file_naming_format_default.setEnabled(active)
148        palette = self.script_palette_normal if active else self.script_palette_readonly
149        self.ui.file_naming_format.setPalette(palette)
150
151        self.ui.ascii_filenames.setEnabled(active)
152        if not IS_WIN:
153            self.ui.windows_compatibility.setEnabled(active)
154
155    def check_formats(self):
156        self.test()
157        self.update_examples()
158
159    def _example_to_filename(self, file):
160        config = get_config()
161        settings = SettingsOverride(config.setting, {
162            'ascii_filenames': self.ui.ascii_filenames.isChecked(),
163            'file_naming_format': self.ui.file_naming_format.toPlainText(),
164            'move_files': self.ui.move_files.isChecked(),
165            'move_files_to': os.path.normpath(self.ui.move_files_to.text()),
166            'rename_files': self.ui.rename_files.isChecked(),
167            'windows_compatibility': self.ui.windows_compatibility.isChecked(),
168        })
169
170        try:
171            if config.setting["enable_tagger_scripts"]:
172                for s_pos, s_name, s_enabled, s_text in config.setting["list_of_scripts"]:
173                    if s_enabled and s_text:
174                        parser = ScriptParser()
175                        parser.eval(s_text, file.metadata)
176            filename = file.make_filename(file.filename, file.metadata, settings)
177            if not settings["move_files"]:
178                return os.path.basename(filename)
179            return filename
180        except ScriptError:
181            return ""
182        except TypeError:
183            return ""
184
185    def update_examples(self):
186        # TODO: Here should be more examples etc.
187        # TODO: Would be nice to show diffs too....
188        example1 = self._example_to_filename(self.example_1())
189        example2 = self._example_to_filename(self.example_2())
190        self.ui.example_filename.setText(example1)
191        self.ui.example_filename_va.setText(example2)
192
193    def load(self):
194        config = get_config()
195        if IS_WIN:
196            self.ui.windows_compatibility.setChecked(True)
197            self.ui.windows_compatibility.setEnabled(False)
198        else:
199            self.ui.windows_compatibility.setChecked(config.setting["windows_compatibility"])
200        self.ui.rename_files.setChecked(config.setting["rename_files"])
201        self.ui.move_files.setChecked(config.setting["move_files"])
202        self.ui.ascii_filenames.setChecked(config.setting["ascii_filenames"])
203        self.ui.file_naming_format.setPlainText(config.setting["file_naming_format"])
204        self.ui.move_files_to.setText(config.setting["move_files_to"])
205        self.ui.move_files_to.setCursorPosition(0)
206        self.ui.move_additional_files.setChecked(config.setting["move_additional_files"])
207        self.ui.move_additional_files_pattern.setText(config.setting["move_additional_files_pattern"])
208        self.ui.delete_empty_dirs.setChecked(config.setting["delete_empty_dirs"])
209        self.update_examples()
210
211    def check(self):
212        self.check_format()
213        if self.ui.move_files.isChecked() and not self.ui.move_files_to.text().strip():
214            raise OptionsCheckError(_("Error"), _("The location to move files to must not be empty."))
215
216    def check_format(self):
217        parser = ScriptParser()
218        try:
219            parser.eval(self.ui.file_naming_format.toPlainText())
220        except Exception as e:
221            raise ScriptCheckError("", str(e))
222        if self.ui.rename_files.isChecked():
223            if not self.ui.file_naming_format.toPlainText().strip():
224                raise ScriptCheckError("", _("The file naming format must not be empty."))
225
226    def save(self):
227        config = get_config()
228        config.setting["windows_compatibility"] = self.ui.windows_compatibility.isChecked()
229        config.setting["ascii_filenames"] = self.ui.ascii_filenames.isChecked()
230        config.setting["rename_files"] = self.ui.rename_files.isChecked()
231        config.setting["file_naming_format"] = self.ui.file_naming_format.toPlainText()
232        self.tagger.window.enable_renaming_action.setChecked(config.setting["rename_files"])
233        config.setting["move_files"] = self.ui.move_files.isChecked()
234        config.setting["move_files_to"] = os.path.normpath(self.ui.move_files_to.text())
235        config.setting["move_additional_files"] = self.ui.move_additional_files.isChecked()
236        config.setting["move_additional_files_pattern"] = self.ui.move_additional_files_pattern.text()
237        config.setting["delete_empty_dirs"] = self.ui.delete_empty_dirs.isChecked()
238        self.tagger.window.enable_moving_action.setChecked(config.setting["move_files"])
239
240    def display_error(self, error):
241        # Ignore scripting errors, those are handled inline
242        if not isinstance(error, ScriptCheckError):
243            super().display_error(error)
244
245    def set_file_naming_format_default(self):
246        self.ui.file_naming_format.setText(self.options[3].default)
247#        self.ui.file_naming_format.setCursorPosition(0)
248
249    def example_1(self):
250        file = File("ticket_to_ride.mp3")
251        file.state = File.NORMAL
252        file.metadata['album'] = 'Help!'
253        file.metadata['title'] = 'Ticket to Ride'
254        file.metadata['~releasecomment'] = '2014 mono remaster'
255        file.metadata['artist'] = 'The Beatles'
256        file.metadata['artistsort'] = 'Beatles, The'
257        file.metadata['albumartist'] = 'The Beatles'
258        file.metadata['albumartistsort'] = 'Beatles, The'
259        file.metadata['tracknumber'] = '7'
260        file.metadata['totaltracks'] = '14'
261        file.metadata['discnumber'] = '1'
262        file.metadata['totaldiscs'] = '1'
263        file.metadata['originaldate'] = '1965-08-06'
264        file.metadata['originalyear'] = '1965'
265        file.metadata['date'] = '2014-09-08'
266        file.metadata['releasetype'] = ['album', 'soundtrack']
267        file.metadata['~primaryreleasetype'] = ['album']
268        file.metadata['~secondaryreleasetype'] = ['soundtrack']
269        file.metadata['releasestatus'] = 'official'
270        file.metadata['releasecountry'] = 'US'
271        file.metadata['~extension'] = 'mp3'
272        file.metadata['musicbrainz_albumid'] = 'd7fbbb0a-1348-40ad-8eef-cd438d4cd203'
273        file.metadata['musicbrainz_albumartistid'] = 'b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d'
274        file.metadata['musicbrainz_artistid'] = 'b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d'
275        file.metadata['musicbrainz_recordingid'] = 'ed052ae1-c950-47f2-8d2b-46e1b58ab76c'
276        file.metadata['musicbrainz_releasetrackid'] = '392639f5-5629-477e-b04b-93bffa703405'
277        return file
278
279    def example_2(self):
280        config = get_config()
281        file = File("track05.mp3")
282        file.state = File.NORMAL
283        file.metadata['album'] = "Coup d'État, Volume 1: Ku De Ta / Prologue"
284        file.metadata['title'] = "I've Got to Learn the Mambo"
285        file.metadata['artist'] = "Snowboy feat. James Hunter"
286        file.metadata['artistsort'] = "Snowboy feat. Hunter, James"
287        file.metadata['albumartist'] = config.setting['va_name']
288        file.metadata['albumartistsort'] = config.setting['va_name']
289        file.metadata['tracknumber'] = '5'
290        file.metadata['totaltracks'] = '13'
291        file.metadata['discnumber'] = '2'
292        file.metadata['totaldiscs'] = '2'
293        file.metadata['discsubtitle'] = "Beat Up"
294        file.metadata['originaldate'] = '2005-07-04'
295        file.metadata['originalyear'] = '2005'
296        file.metadata['date'] = '2005-07-04'
297        file.metadata['releasetype'] = ['album', 'compilation']
298        file.metadata['~primaryreleasetype'] = 'album'
299        file.metadata['~secondaryreleasetype'] = 'compilation'
300        file.metadata['releasestatus'] = 'official'
301        file.metadata['releasecountry'] = 'AU'
302        file.metadata['compilation'] = '1'
303        file.metadata['~multiartist'] = '1'
304        file.metadata['~extension'] = 'mp3'
305        file.metadata['musicbrainz_albumid'] = '4b50c71e-0a07-46ac-82e4-cb85dc0c9bdd'
306        file.metadata['musicbrainz_recordingid'] = 'b3c487cb-0e55-477d-8df3-01ec6590f099'
307        file.metadata['musicbrainz_releasetrackid'] = 'f8649a05-da39-39ba-957c-7abf8f9012be'
308        file.metadata['musicbrainz_albumartistid'] = '89ad4ac3-39f7-470e-963a-56509c546377'
309        file.metadata['musicbrainz_artistid'] = ['7b593455-d207-482c-8c6f-19ce22c94679',
310                                                 '9e082466-2390-40d1-891e-4803531f43fd']
311        return file
312
313    def move_files_to_browse(self):
314        path = QtWidgets.QFileDialog.getExistingDirectory(self, "", self.ui.move_files_to.text())
315        if path:
316            path = os.path.normpath(path)
317            self.ui.move_files_to.setText(path)
318
319    def test(self):
320        self.ui.renaming_error.setStyleSheet("")
321        self.ui.renaming_error.setText("")
322        try:
323            self.check_format()
324        except ScriptCheckError as e:
325            self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR)
326            self.ui.renaming_error.setText(e.info)
327            return
328
329
330register_options_page(RenamingOptionsPage)
331