├── src ├── exception.py ├── version.py ├── constant.py ├── __init__.py ├── gabtag.gresource.xml ├── selection_handler.py ├── dir_manager.py ├── extension_manager.py ├── audio_getter.py ├── audio_extension_handler.py ├── crawler_modification.py ├── meson.build ├── crawler_directory.py ├── gabtag.in ├── main.py ├── treeview.py ├── crawler_data.py ├── audio_ogg_file_handler.py ├── audio_mp3_file_handler.py ├── window_gtk.py ├── tools.py ├── event_machine.py ├── view.py ├── controller.py ├── model.py └── window.ui ├── tests ├── __init__.py ├── test_treeview.py ├── test_audio_ogg_file_handler.py ├── test_audio_mp3_file_handler.py ├── test_controller.py ├── test_crawler_data.py ├── test_tools.py ├── test_view.py ├── test_model.py └── test_event_machine.py ├── .coveragerc ├── .flake8 ├── po ├── meson.build ├── LINGUAS ├── POTFILES ├── gabtag.pot ├── tr.po ├── es.po ├── it.po ├── sv.po ├── pt_BR.po └── fr.po ├── .mypy.ini ├── meson_options.txt ├── requirements.txt ├── .tx └── config ├── data ├── com.github.lachhebo.Gabtag.gschema.xml ├── com.github.lachhebo.Gabtag.desktop.in ├── icons │ ├── meson.build │ └── hicolor │ │ ├── symbolic │ │ └── apps │ │ │ └── com.github.lachhebo.Gabtag-symbolic.svg │ │ └── scalable │ │ └── apps │ │ ├── com.github.lachhebo.Gabtag.svg │ │ └── com.github.lachhebo.Gabtag.Devel.svg ├── meson.build └── com.github.lachhebo.Gabtag.appdata.xml.in ├── .gitignore ├── meson.build ├── .github └── workflows │ ├── flatpak.yml │ ├── gabtag-testing.yml │ └── flathub-publish.yml ├── Makefile ├── deploy.sh ├── README.md └── com.github.lachhebo.Gabtag.Devel.json /src/exception.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_treeview.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "15" 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = src/window_gtk.py -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 130 3 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('gabtag', preset: 'glib') 2 | -------------------------------------------------------------------------------- /src/constant.py: -------------------------------------------------------------------------------- 1 | HANDLED_EXTENSIONS = ["mp3", "ogg"] 2 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | ROOT_PATH = pathlib.Path(__file__).parent 4 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | # please keep this list sorted alphabetically 2 | es 3 | fr 4 | it 5 | pt_BR 6 | sv 7 | tr 8 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('devel', type: 'boolean', value: false, description: 'If this is a development build') 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mutagen 2 | pycairo 3 | musicbrainzngs 4 | pygobject 5 | Pillow 6 | flake8 7 | pytest 8 | pytest-coverage 9 | pytest-bdd 10 | pathlib 11 | mypy 12 | codespell 13 | -------------------------------------------------------------------------------- /src/gabtag.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | window.ui 5 | 6 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [gabtag.gabtag-pot] 5 | file_filter = po/.po 6 | minimum_perc = 0 7 | source_file = po/gabtag.pot 8 | source_lang = en_GB 9 | type = PO 10 | 11 | -------------------------------------------------------------------------------- /data/com.github.lachhebo.Gabtag.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/com.github.lachhebo.Gabtag.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Gabtag 3 | Comment= Modify audio tags 4 | Icon=@APP_ID@ 5 | Exec=gabtag 6 | Terminal=false 7 | Type=Application 8 | Categories=AudioVideo;Audio;AudioVideoEditing; 9 | StartupNotify=true 10 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/com.github.lachhebo.Gabtag.desktop.in 2 | data/com.github.lachhebo.Gabtag.appdata.xml.in 3 | data/com.github.lachhebo.Gabtag.gschema.xml 4 | src/event_machine.py 5 | src/main.py 6 | src/tools.py 7 | src/treeview.py 8 | src/window.ui 9 | src/window_gtk.py 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .flatpak/ 2 | src/window.ui~ 3 | tests/__pycache__ 4 | src/__pycache__ 5 | __pycache__ 6 | .flatpak-builder/ 7 | dev/ 8 | .tx/ 9 | venv/ 10 | .idea/ 11 | build.sh 12 | python3-gabtag/ 13 | gabtag-repo/ 14 | *.ipynb 15 | .coverage 16 | .vscode/* 17 | htmlcov/* 18 | .ipynb_checkpoints/* 19 | *~ 20 | ~* 21 | -------------------------------------------------------------------------------- /src/selection_handler.py: -------------------------------------------------------------------------------- 1 | class SelectionHandler: 2 | def __init__(self): 3 | self.selection = None 4 | self.has_directory_change = False 5 | 6 | def update_dir(self): 7 | self.selection = None 8 | self.has_directory_change = True 9 | 10 | 11 | SELECTION = SelectionHandler() 12 | -------------------------------------------------------------------------------- /src/dir_manager.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | class DirectoryManager: 5 | def __init__(self): 6 | self.directory: str = "" 7 | self.file_names: List = [] 8 | self.is_open_directory = False 9 | 10 | def clear(self): 11 | self.directory = "" 12 | self.file_names = [] 13 | self.is_open_directory = False 14 | 15 | 16 | DIR_MANAGER = DirectoryManager() 17 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | scalable_dir = join_paths('hicolor', 'scalable', 'apps') 2 | install_data( 3 | join_paths(scalable_dir, ('@0@.svg').format(app_id)), 4 | install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) 5 | ) 6 | 7 | symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') 8 | install_data( 9 | join_paths(symbolic_dir, 'com.github.lachhebo.Gabtag-symbolic.svg'), 10 | install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir), 11 | rename: '@0@-symbolic.svg'.format(app_id) 12 | ) 13 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('gabtag', 2 | version: '15', 3 | meson_version: '>= 0.59.0', 4 | default_options: [ 'warning_level=2', 5 | ], 6 | ) 7 | 8 | i18n = import('i18n') 9 | gnome = import('gnome') 10 | 11 | if get_option('devel') 12 | app_id = 'com.github.lachhebo.Gabtag.Devel' 13 | else 14 | app_id = 'com.github.lachhebo.Gabtag' 15 | endif 16 | 17 | subdir('data') 18 | subdir('src') 19 | subdir('po') 20 | 21 | gnome.post_install( 22 | glib_compile_schemas: true, 23 | gtk_update_icon_cache: true, 24 | update_desktop_database: true, 25 | ) 26 | -------------------------------------------------------------------------------- /src/extension_manager.py: -------------------------------------------------------------------------------- 1 | from .constant import HANDLED_EXTENSIONS 2 | 3 | 4 | def get_file_extension(filename): 5 | """ 6 | return the file extension. 7 | """ 8 | namelist = filename.split(".") 9 | return namelist[-1] 10 | 11 | 12 | def is_extension_managed(filename): 13 | """ 14 | Check if the file extension is handled by Gabtag 15 | input : a filename (string) 16 | output : a bool 17 | """ 18 | 19 | extension = get_file_extension(filename) 20 | 21 | if extension in HANDLED_EXTENSIONS: 22 | return True 23 | else: 24 | return False 25 | -------------------------------------------------------------------------------- /.github/workflows/flatpak.yml: -------------------------------------------------------------------------------- 1 | name: Flatpak 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | pull_request: 7 | - "*" 8 | 9 | jobs: 10 | flatpak: 11 | name: "Flatpak" 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 15 | options: --privileged 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 19 | with: 20 | manifest-path: com.github.lachhebo.Gabtag.Devel.json 21 | bundle: com.github.lachhebo.Gabtag.Devel.flatpak 22 | cache-key: flatpak-builder-${{ github.sha }} -------------------------------------------------------------------------------- /.github/workflows/gabtag-testing.yml: -------------------------------------------------------------------------------- 1 | name: Run Python Tests 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | pull_request: 7 | - "*" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3.0.2 14 | 15 | - name: Install Python 3 16 | uses: actions/setup-python@v4.2.0 17 | with: 18 | python-version: 3.8 19 | 20 | - name: Install dependencies 21 | run: | 22 | pip install flake8 mypy 23 | 24 | 25 | - name: Run flake8 26 | run: | 27 | flake8 . 28 | 29 | - name: Run mypy 30 | run: | 31 | mypy src/ 32 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/com.github.lachhebo.Gabtag-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/flathub-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload to Flathub 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3.0.2 18 | - name: Build and publish 19 | env: 20 | PASSWORD_DEPLOYEMENT: ${{ secrets.PASSWORD_DEPLOYEMENT }} 21 | run: | 22 | ./deploy.sh $(cat src/version.py | cut -d " " -f 3) 23 | -------------------------------------------------------------------------------- /src/audio_getter.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from .audio_mp3_file_handler import Mp3FileHandler 4 | from .audio_ogg_file_handler import OggFileHandler 5 | from .extension_manager import get_file_extension 6 | 7 | 8 | def get_file_manager(filename, directory): 9 | """ 10 | return the correct handler for the file 11 | input : a file (string), a directory (string) 12 | output : an Handler or None 13 | """ 14 | 15 | ext = get_file_extension(filename) 16 | if ext == "mp3": 17 | # print("read mp3: ",filename) 18 | return Mp3FileHandler(path.join(directory, filename)) 19 | elif ext == "ogg": 20 | return OggFileHandler(path.join(directory, filename)) 21 | else: 22 | return None 23 | -------------------------------------------------------------------------------- /src/audio_extension_handler.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Dict 3 | 4 | 5 | class AudioExtensionHandler: 6 | @abstractmethod 7 | def get_tag(self, tag_key) -> str: 8 | """return the value of the tag""" 9 | 10 | @abstractmethod 11 | def set_tag(self, tag_key: str, tag_value: str) -> None: 12 | """modify the value of the tag in the audio file""" 13 | 14 | @abstractmethod 15 | def save_modifications(self) -> None: 16 | """save all previous modification made by the user""" 17 | 18 | @abstractmethod 19 | def check_tag_existence(self, key: str) -> bool: 20 | """return True if the tag exists, False otherwise""" 21 | 22 | @abstractmethod 23 | def get_tags(self) -> Dict: 24 | """return a dictionary containing the value of each tag""" 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | setupenv: 2 | python -m virtualenv venv 3 | sudo pacman -Su gobject-introspection flatpak-builder --needed 4 | source venv/bin/activate 5 | pip install - r requirements.txt 6 | flatpak install flathub org.gnome.Sdk/x86_64/42 7 | 8 | 9 | test: 10 | pytest tests --cov=src/ 11 | 12 | lint: 13 | black src/ 14 | black tests/ 15 | flake8 src 16 | flake8 tests 17 | codespell -S venv,po 18 | 19 | install: 20 | flatpak install flathub org.gnome.Sdk/x86_64/42 21 | flatpak install flathub org.gnome.Platform/x86_64/42 22 | flatpak-builder --repo=gabtag-repo python3-gabtag com.github.lachhebo.Gabtag.Devel.json --force-clean 23 | flatpak --user remote-add --no-gpg-verify --if-not-exists gabtag-repo gabtag-repo 24 | flatpak --user install gabtag-repo com.github.lachhebo.Gabtag.Devel --reinstall -y 25 | 26 | run: 27 | flatpak run com.github.lachhebo.Gabtag.Devel 28 | -------------------------------------------------------------------------------- /src/crawler_modification.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from typing import Dict, List 3 | 4 | from .dir_manager import DIR_MANAGER 5 | 6 | from .crawler_data import DATA_CRAWLER 7 | from .tools import is_selection_valid 8 | from .treeview import TREE_VIEW 9 | from .view import VIEW 10 | 11 | 12 | class CrawlerModification(Thread): 13 | def __init__(self, modification: Dict, name_files: List): 14 | Thread.__init__(self) 15 | self.modification = modification 16 | self.file_names = name_files 17 | 18 | def run(self): 19 | 20 | names_file = DATA_CRAWLER.update_data_crawled( 21 | self.modification, DIR_MANAGER.directory 22 | ) 23 | TREE_VIEW.manage_crawled(names_file) 24 | 25 | if is_selection_valid(self.file_names): 26 | data_scrapped = DATA_CRAWLER.get_tags(self.file_names) 27 | if is_selection_valid(self.file_names): 28 | if data_scrapped is not None: 29 | VIEW.show_mbz(data_scrapped) 30 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | RELEASE_DATE=$(date +%Y-%m-%d) 2 | LATEST_VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed -En 's/v//p') 3 | LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null) 4 | RELEASE_NUMBER="${1}" 5 | 6 | 7 | echo $(ls) 8 | 9 | git config --global user.email "ismael.lachheb@protonmail.com" 10 | git config --global user.name "Ismaël Lachheb" 11 | 12 | ## update version information 13 | 14 | sed -i 's/version="'${LATEST_VERSION}'"/version="'${RELEASE_NUMBER}'"/g' data/com.github.lachhebo.Gabtag.appdata.xml.in 15 | sed -i 's/release date="'[1234567890-]*'"/release date="'${RELEASE_DATE}'"/g' data/com.github.lachhebo.Gabtag.appdata.xml.in 16 | sed -i 's/'${LATEST_VERSION}'/'${RELEASE_NUMBER}'/g' src/version.py 17 | 18 | 19 | ## commit those modification and create a new release tag 20 | git commit -am 'upgrade version to '${RELEASE_NUMBER} 21 | git tag -a ${RELEASE_NUMBER} -m "version"${RELEASE_NUMBER} 22 | git push 23 | git push --tags 24 | 25 | 26 | ## clone flathub repo and update manifest to match latest tags 27 | 28 | git clone https://github.com/flathub/com.github.lachhebo.Gabtag.git 29 | cd com.github.lachhebo.Gabtag 30 | sed -i 's/"tag": "'${LATEST_TAG}'"/"tag": "'${RELEASE_NUMBER}'"/g' com.github.lachhebo.Gabtag.json 31 | git commit -am "upgrade to new version "${RELEASE_NUMBER} 32 | git push https://lachhebo:${PASSWORD_DEPLOYEMENT}@github.com/flathub/com.github.lachhebo.Gabtag.git 33 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | moduledir = join_paths(pkgdatadir, 'gabtag') 3 | 4 | gnome.compile_resources('gabtag', 5 | 'gabtag.gresource.xml', 6 | gresource_bundle: true, 7 | install: true, 8 | install_dir: pkgdatadir, 9 | ) 10 | 11 | python = import('python') 12 | 13 | conf = configuration_data() 14 | conf.set('APP_ID', app_id) 15 | conf.set('VERSION', meson.project_version()) 16 | conf.set('DEVEL', get_option('devel')) 17 | conf.set('PYTHON', python.find_installation('python3').full_path()) 18 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) 19 | conf.set('pkgdatadir', pkgdatadir) 20 | 21 | configure_file( 22 | input: 'gabtag.in', 23 | output: meson.project_name(), 24 | configuration: conf, 25 | install: true, 26 | install_dir: get_option('bindir') 27 | ) 28 | 29 | gabtag_sources = [ 30 | '__init__.py', 31 | 'main.py', 32 | 'version.py', 33 | 34 | 'exception.py', 35 | 'constant.py', 36 | 'extension_manager.py', 37 | 'dir_manager.py', 38 | 'selection_handler.py', 39 | 'tools.py', 40 | 'model.py', 41 | 42 | 'treeview.py', 43 | 'view.py', 44 | 'window_gtk.py', 45 | 'controller.py', 46 | 'event_machine.py', 47 | 48 | 'audio_extension_handler.py', 49 | 'audio_getter.py', 50 | 'audio_mp3_file_handler.py', 51 | 'audio_ogg_file_handler.py', 52 | 53 | 'crawler_data.py', 54 | 'crawler_directory.py', 55 | 'crawler_modification.py', 56 | ] 57 | 58 | install_data(gabtag_sources, install_dir: moduledir) 59 | -------------------------------------------------------------------------------- /src/crawler_directory.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | from .crawler_data import DATA_CRAWLER 4 | from .dir_manager import DIR_MANAGER 5 | from .tools import get_file_list 6 | from .treeview import TREE_VIEW 7 | 8 | 9 | def split(file_list, n=1): 10 | k, m = divmod(len(file_list), n) 11 | return ( 12 | file_list[i * k + min(i, m): (i + 1) * k + min(i + 1, m)] for i in range(n) 13 | ) 14 | 15 | 16 | class CrawlerDirectory(Thread): 17 | def __init__(self, directory): 18 | Thread.__init__(self) 19 | self.directory = directory 20 | 21 | def run(self): 22 | file_list = get_file_list(self.directory) 23 | file_list_pool = split(file_list) 24 | 25 | thread_pool = [] 26 | for file_list_slice in file_list_pool: 27 | thread = Thread( 28 | target=DATA_CRAWLER.get_data_from_online, 29 | args=(file_list_slice, DIR_MANAGER.directory), 30 | ) 31 | thread.start() 32 | thread_pool.append(thread) 33 | 34 | marked = [] 35 | while True in [thread.is_alive() for thread in thread_pool]: 36 | self.check_and_mark_file_crawled(marked) 37 | 38 | self.check_and_mark_file_crawled(marked) 39 | 40 | def check_and_mark_file_crawled(self, marked): 41 | for file_name in DATA_CRAWLER.tag_founds.copy().keys(): 42 | if file_name not in marked and self.directory == DIR_MANAGER.directory: 43 | TREE_VIEW.manage_crawled([file_name]) 44 | marked.append(file_name) 45 | -------------------------------------------------------------------------------- /src/gabtag.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # gabtag.in 4 | # 5 | # Copyright 2019 Ismaïl Lachheb 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (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, see . 19 | 20 | import os 21 | import sys 22 | import signal 23 | import gettext 24 | import locale 25 | 26 | APP_ID = '@APP_ID@' 27 | VERSION = '@VERSION@' 28 | DEVEL = '@DEVEL@' == "True" 29 | pkgdatadir = '@pkgdatadir@' 30 | localedir = '@localedir@' 31 | 32 | sys.path.insert(1, pkgdatadir) 33 | signal.signal(signal.SIGINT, signal.SIG_DFL) 34 | locale.bindtextdomain('gabtag', localedir) 35 | locale.textdomain('gabtag') 36 | gettext.bindtextdomain('gabtag', localedir) 37 | gettext.textdomain('gabtag') 38 | 39 | 40 | if __name__ == '__main__': 41 | import gi 42 | 43 | from gi.repository import Gio 44 | resource = Gio.Resource.load(os.path.join(pkgdatadir, 'gabtag.gresource')) 45 | resource._register() 46 | 47 | from gabtag import main 48 | sys.exit(main.main(app_id=APP_ID, version=VERSION, devel=DEVEL)) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # GabTag 4 | 5 | [![codecov](https://codecov.io/gh/lachhebo/GabTag/branch/master/graph/badge.svg)](https://codecov.io/gh/lachhebo/GabTag) 6 | 7 | GabTag is a Linux audio tagging tool written in GTK 4 and Adwaita, which makes it very suitable for GTK based desktop users. 8 | 9 | It allows users to select several files and modify their tags. It is also possible to let GabTag automatically find tags for an audio file using MusicBrainz. 10 | 11 |

12 |

13 | 14 | 15 |

Download on Flathub

16 | 17 | ## Features : 18 | 19 | - Add, modify or delete basic tags (title, album, artist, genre) 20 | - other strings tags and labels 21 | - Cover tag 22 | - Modify several file at the same time. 23 | - MP3 File handled 24 | - bold font on modified tags and files 25 | - Automatic completion of tags (from online data) 26 | 27 | ## Contributing 28 | 29 | To setup development environment on arch based distro: 30 | 31 | make setupenv 32 | 33 | To run test: 34 | 35 | make tests 36 | 37 | To install: 38 | 39 | make install 40 | 41 | to run: 42 | 43 | make run 44 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | config = configuration_data() 2 | config.set('APP_ID', app_id) 3 | 4 | desktop_conf = configure_file( 5 | input: 'com.github.lachhebo.Gabtag.desktop.in', 6 | output: '@0@.desktop.in'.format(app_id), 7 | configuration: config 8 | ) 9 | 10 | desktop_file = i18n.merge_file( 11 | input: desktop_conf, 12 | output: '@0@.desktop'.format(app_id), 13 | type: 'desktop', 14 | po_dir: '../po', 15 | install: true, 16 | install_dir: join_paths(get_option('datadir'), 'applications') 17 | ) 18 | 19 | desktop_utils = find_program('desktop-file-validate', required: false) 20 | if desktop_utils.found() 21 | test('Validate desktop file', desktop_utils, 22 | args: [desktop_file] 23 | ) 24 | endif 25 | 26 | appstream_conf = configure_file( 27 | input: 'com.github.lachhebo.Gabtag.appdata.xml.in', 28 | output: '@0@.appdata.xml.in'.format(app_id), 29 | configuration: config 30 | ) 31 | 32 | appstream_file = i18n.merge_file( 33 | input: appstream_conf, 34 | output: '@0@.appdata.xml'.format(app_id), 35 | po_dir: '../po', 36 | install: true, 37 | install_dir: join_paths(get_option('datadir'), 'appdata') 38 | ) 39 | 40 | appstream_util = find_program('appstream-util', required: false) 41 | if appstream_util.found() 42 | test('Validate appstream file', appstream_util, 43 | args: ['validate', appstream_file] 44 | ) 45 | endif 46 | 47 | install_data('com.github.lachhebo.Gabtag.gschema.xml', 48 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 49 | ) 50 | 51 | compile_schemas = find_program('glib-compile-schemas', required: false) 52 | if compile_schemas.found() 53 | test('Validate schema file', compile_schemas, 54 | args: ['--strict', '--dry-run', meson.current_source_dir()] 55 | ) 56 | endif 57 | 58 | subdir('icons') 59 | -------------------------------------------------------------------------------- /data/com.github.lachhebo.Gabtag.appdata.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | @APP_ID@ 4 | GabTag 5 | CC0-1.0 6 | GPL-3.0-or-later 7 | 8 | 9 | 10 | ​ 11 | AudioVideo 12 | 13 | An audio tagging tool 14 | 15 |

16 | GabTag is a Linux audio tagging tool written in GTK 4 and Adwaita, 17 | which makes it very suitable for GTK based desktop users. 18 |

19 |

20 | It allows users to select several files and modify their tags. It is also possible to let GabTag automatically 21 | find tags for an audio file using MusicBrainz. 22 |

23 |
24 | 25 | 26 | https://raw.githubusercontent.com/lachhebo/GabTag/screenshots/Gabtag_v13_2.png 27 | 28 | 29 | https://raw.githubusercontent.com/lachhebo/GabTag/screenshots/Gabtag_v13_1.png 30 | 31 | 32 | ismael.lachheb@protonmail.com 33 | Ismaïl Lachheb 34 | @APP_ID@.desktop 35 | https://github.com/lachhebo/gabtag 36 | https://github.com/lachhebo/gabtag/issues 37 | https://paypal.me/lachhebo 38 | 39 | 40 | audio tagger 41 | music tagging 42 | audio metadata 43 | 44 |
45 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # main.py 2 | # 3 | # Copyright 2019 Ismaïl Lachheb 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import gi 19 | import sys 20 | 21 | from .window_gtk import GabtagWindow 22 | 23 | gi.require_version("Adw", "1") 24 | 25 | from gi.repository import Adw, Gio, GLib, GObject # noqa: E402 26 | 27 | 28 | class Application(Adw.Application): 29 | 30 | app_id = GObject.Property(type=str) 31 | version = GObject.Property(type=str) 32 | devel = GObject.Property(type=bool, default=False) 33 | 34 | def __init__(self, app_id: str, version: str, devel: bool, *args, **kwargs): 35 | super().__init__(flags=Gio.ApplicationFlags.HANDLES_OPEN, *args, **kwargs) 36 | 37 | self.app_id = app_id 38 | self.version = version 39 | self.devel = devel 40 | 41 | GLib.set_application_name("GabTag") 42 | GLib.set_prgname(self.app_id) 43 | 44 | def do_activate(self): 45 | win = self.props.active_window 46 | if not win: 47 | win = GabtagWindow( 48 | application=self, 49 | app_id=self.app_id, 50 | version=self.version, 51 | devel=self.devel, 52 | ) 53 | win.set_default_icon_name(self.app_id) 54 | win.present() 55 | 56 | 57 | def main(app_id, version, devel): 58 | app = Application(app_id, version, devel) 59 | return app.run(sys.argv) 60 | -------------------------------------------------------------------------------- /tests/test_audio_ogg_file_handler.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, Mock, call 2 | 3 | from src.audio_ogg_file_handler import OggFileHandler 4 | 5 | TESTED_MODULE = "src.audio_ogg_file_handler" 6 | 7 | 8 | def test_get_extension__return_ogg(): 9 | # given 10 | 11 | # when 12 | result = OggFileHandler.get_extension() 13 | 14 | # then 15 | assert result == ".ogg" 16 | 17 | 18 | @patch(f"{TESTED_MODULE}.OGG") 19 | def test_get_one_tag__return_empty_string_if_no_id3(m_ogg): 20 | # given 21 | oggfilehandler = OggFileHandler("fake_path.ogg") 22 | oggfilehandler.id3 = Mock() 23 | oggfilehandler.id3.get.return_value = [] 24 | 25 | # when 26 | result = oggfilehandler.get_one_tag("title", "text") 27 | 28 | # then 29 | assert result == "" 30 | 31 | 32 | @patch(f"{TESTED_MODULE}.MP3") 33 | def test_get_one_tag__return_tag_if_id3_with_text(m_ogg): 34 | # given 35 | oggfilehandler = OggFileHandler("fake_path.ogg") 36 | oggfilehandler.id3 = Mock() 37 | fake_tag = Mock() 38 | fake_tag.text = ["title"] 39 | oggfilehandler.id3.get.return_value = [fake_tag] 40 | 41 | # when 42 | result = oggfilehandler.get_one_tag("title", "text") 43 | 44 | # then 45 | assert result == "title" 46 | 47 | 48 | @patch(f"{TESTED_MODULE}.MP3") 49 | def test_get_one_tag__return_tag_if_id3_with_data(m_ogg): 50 | # given 51 | oggfilehandler = OggFileHandler("fake_path.ogg") 52 | oggfilehandler.id3 = Mock() 53 | fake_tag = Mock() 54 | fake_tag.data = ["data"] 55 | oggfilehandler.id3.get.return_value = [fake_tag] 56 | 57 | # when 58 | result = oggfilehandler.get_one_tag("title", "data") 59 | 60 | # then 61 | assert result == ["data"] 62 | 63 | 64 | @patch(f"{TESTED_MODULE}.MP3") 65 | @patch(f"{TESTED_MODULE}.OgFileHandler.get_one_tag") 66 | def test_get_tag_research__return_title_artist_and_album(m_get, m_ogg): 67 | # given 68 | oggfilehandler = OggFileHandler("fake_path.ogg") 69 | oggfilehandler.id3 = Mock() 70 | calls = [call("TITLE", "text"), call("ARTIST", "text"), call("ALBUM", "text")] 71 | 72 | # when 73 | oggfilehandler.get_tag_research() 74 | 75 | # then 76 | m_get.assert_has_calls(calls=calls, any_order=True) 77 | -------------------------------------------------------------------------------- /tests/test_audio_mp3_file_handler.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, Mock, call 2 | 3 | from src.audio_mp3_file_handler import Mp3FileHandler 4 | 5 | TESTED_MODULE = "src.audio_mp3_file_handler" 6 | 7 | 8 | def test_get_extension__return_mp3(): 9 | # given 10 | 11 | # when 12 | result = Mp3FileHandler.get_extension() 13 | 14 | # then 15 | assert result == ".mp3" 16 | 17 | 18 | @patch(f"{TESTED_MODULE}.MP3") 19 | def test_get_one_tag__return_empty_string_if_no_id3(m_mp3): 20 | # given 21 | mp3filehandler = Mp3FileHandler("fake_path.mp3") 22 | mp3filehandler.id3 = Mock() 23 | mp3filehandler.id3.getall.return_value = [] 24 | 25 | # when 26 | result = mp3filehandler.get_one_tag("title", "text") 27 | 28 | # then 29 | assert result == "" 30 | 31 | 32 | @patch(f"{TESTED_MODULE}.MP3") 33 | def test_get_one_tag__return_tag_if_id3_with_text(m_mp3): 34 | # given 35 | mp3filehandler = Mp3FileHandler("fake_path.mp3") 36 | mp3filehandler.id3 = Mock() 37 | fake_tag = Mock() 38 | fake_tag.text = ["title"] 39 | mp3filehandler.id3.getall.return_value = [fake_tag] 40 | 41 | # when 42 | result = mp3filehandler.get_one_tag("title", "text") 43 | 44 | # then 45 | assert result == "title" 46 | 47 | 48 | @patch(f"{TESTED_MODULE}.MP3") 49 | def test_get_one_tag__return_tag_if_id3_with_data(m_mp3): 50 | # given 51 | mp3filehandler = Mp3FileHandler("fake_path.mp3") 52 | mp3filehandler.id3 = Mock() 53 | fake_tag = Mock() 54 | fake_tag.data = ["data"] 55 | mp3filehandler.id3.getall.return_value = [fake_tag] 56 | 57 | # when 58 | result = mp3filehandler.get_one_tag("title", "data") 59 | 60 | # then 61 | assert result == ["data"] 62 | 63 | 64 | @patch(f"{TESTED_MODULE}.MP3") 65 | @patch(f"{TESTED_MODULE}.Mp3FileHandler.get_one_tag") 66 | def test_get_tag_research__return_title_artist_and_album(m_get, m_mp3): 67 | # given 68 | mp3filehandler = Mp3FileHandler("fake_path.mp3") 69 | mp3filehandler.id3 = Mock() 70 | calls = [call("TIT2", "text"), call("TPE1", "text"), call("TALB", "text")] 71 | 72 | # when 73 | mp3filehandler.get_tag_research() 74 | 75 | # then 76 | m_get.assert_has_calls(calls=calls, any_order=True) 77 | -------------------------------------------------------------------------------- /src/treeview.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import gi 4 | from gi.repository import Gtk 5 | 6 | import gettext 7 | 8 | gi.require_version("Gtk", "4.0") 9 | 10 | _ = gettext.gettext 11 | 12 | 13 | class TreeView: 14 | def __init__(self, store, view): 15 | self.store = store 16 | self.view = view 17 | 18 | def add_columns(self): 19 | if self.store is not None and self.view is not None: 20 | self.view.set_model(self.store) 21 | 22 | renderer_filename = Gtk.CellRendererText() 23 | column_filename = Gtk.TreeViewColumn( 24 | _("Name"), renderer_filename, text=0, weight=2 25 | ) 26 | 27 | renderer_data = Gtk.CellRendererText() 28 | column_data_gathered = Gtk.TreeViewColumn( 29 | _("Data"), renderer_data, text=1, weight=2 30 | ) 31 | 32 | self.view.append_column(column_data_gathered) 33 | self.view.append_column(column_filename) 34 | 35 | def update_tree_view_list(self, file_names: List): 36 | """ 37 | Erase the list in the tree view and then update it with filename 38 | with extension handled by GabTag 39 | """ 40 | 41 | self.store.clear() 42 | 43 | for name_file in file_names: 44 | self.store.append([name_file, _("No"), 400]) 45 | 46 | def manage_crawled(self, name_files, add=True): 47 | line_number = -1 48 | i = 0 49 | 50 | for filename in name_files: 51 | for row in self.store: 52 | if row[0] == filename: 53 | line_number = i 54 | else: 55 | i = i + 1 56 | 57 | if line_number != -1: 58 | path = Gtk.TreePath(line_number) 59 | list_iterator = self.store.get_iter(path) 60 | if add: 61 | self.store.set_value(list_iterator, 1, _("Yes")) 62 | else: 63 | self.store.set_value(list_iterator, 1, _("No")) 64 | i = 0 65 | 66 | def manage_bold_font(self, name_files, add=True): 67 | line_number = -1 68 | i = 0 69 | 70 | for filename in name_files: 71 | for row in self.store: 72 | if row[0] == filename: 73 | line_number = i 74 | else: 75 | i = i + 1 76 | 77 | if line_number != -1: 78 | path = Gtk.TreePath(line_number) 79 | list_iterator = self.store.get_iter(path) 80 | if add: 81 | self.store.set_value(list_iterator, 2, 700) 82 | else: 83 | self.store.set_value(list_iterator, 2, 400) 84 | i = 0 85 | 86 | 87 | TREE_VIEW = TreeView(None, None) 88 | -------------------------------------------------------------------------------- /tests/test_controller.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, Mock 2 | 3 | import gi 4 | 5 | from src.controller import Controller 6 | 7 | gi.require_version("Gtk", "4.0") 8 | 9 | 10 | TESTED_MODULE = "src.controller" 11 | 12 | 13 | @patch(f"{TESTED_MODULE}.DIR_MANAGER") 14 | @patch(f"{TESTED_MODULE}.TREE_VIEW") 15 | @patch(f"{TESTED_MODULE}.MODEL") 16 | def test_update_directory__reset_modifications(m_model, m_tree, m_dir): 17 | # given 18 | controller = Controller() 19 | fake_directory = "fake" 20 | 21 | # when 22 | controller.update_directory(fake_directory) 23 | 24 | # then 25 | m_model.reset_all.assert_called() 26 | 27 | 28 | @patch(f"{TESTED_MODULE}.VIEW") 29 | @patch(f"{TESTED_MODULE}.MODEL") 30 | def test_reset_all__erase_view_and_model(m_model, m_view): 31 | # given 32 | controller = Controller() 33 | 34 | # when 35 | controller.reset_all() 36 | 37 | # then 38 | m_model.reset_all.assert_called() 39 | m_view.erase.assert_called() 40 | 41 | 42 | @patch(f"{TESTED_MODULE}.TREE_VIEW") 43 | @patch(f"{TESTED_MODULE}.MODEL") 44 | def test_reset_one__erase_view_and_model(m_model, m_tree_view): 45 | # given 46 | controller = Controller() 47 | 48 | # when 49 | controller.reset_one(["fake_file.mp3"]) 50 | 51 | # then 52 | m_model.reset.assert_called() 53 | m_tree_view.manage_bold_font.assert_called_with(["fake_file.mp3"], add=False) 54 | 55 | 56 | @patch(f"{TESTED_MODULE}.TREE_VIEW") 57 | @patch(f"{TESTED_MODULE}.CrawlerModification") 58 | @patch(f"{TESTED_MODULE}.MODEL") 59 | def test_crawl_thread_modification__erase_view_and_model(m_model, m_crawl, m_tree_view): 60 | # given 61 | controller = Controller() 62 | m_crawl_ins = Mock() 63 | m_crawl.return_value = m_crawl_ins 64 | 65 | # when 66 | controller.crawl_thread_modification() 67 | 68 | # then 69 | m_crawl_ins.start.assert_called() 70 | m_model.save_modifications.assert_called() 71 | 72 | 73 | @patch(f"{TESTED_MODULE}.TREE_VIEW") 74 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 75 | @patch(f"{TESTED_MODULE}.MODEL") 76 | @patch(f"{TESTED_MODULE}.CrawlerModification") 77 | def test_save_some_files__erase_view_and_model(m_crawl, m_model, m_get, m_tree_view): 78 | # given 79 | controller = Controller() 80 | m_crawl_ins = Mock() 81 | m_crawl.return_value = m_crawl_ins 82 | 83 | # when 84 | controller.save_some_files() 85 | 86 | # then 87 | m_crawl_ins.start.assert_called() 88 | m_model.save_modifications.assert_called() 89 | 90 | 91 | @patch(f"{TESTED_MODULE}.Controller") 92 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 93 | @patch(f"{TESTED_MODULE}.CrawlerModification") 94 | def test_reset_some_files__erase_view_and_model(m_crawl, m_get, m_controller): 95 | # given 96 | controller = Controller() 97 | m_crawl_ins = Mock() 98 | m_crawl.return_value = m_crawl_ins 99 | 100 | # when 101 | controller.reset_some_files() 102 | 103 | # then 104 | m_controller.reset_one.assert_called() 105 | -------------------------------------------------------------------------------- /tests/test_crawler_data.py: -------------------------------------------------------------------------------- 1 | from src.crawler_data import DataCrawler 2 | from unittest.mock import Mock, patch 3 | 4 | TESTED_MODULE = "src.crawler_data" 5 | 6 | 7 | @patch(f"{TESTED_MODULE}.DataCrawler.search_by_filename") 8 | @patch(f"{TESTED_MODULE}.get_file_manager") 9 | def test_crawl_one_file__when_crawling_we_start_by_searching_by_filename_if_no_title_nor_artist( 10 | mock_file, mock_search 11 | ): 12 | # given 13 | data_crawler = DataCrawler() 14 | audio = Mock() 15 | audio.get_tag_research.return_value = ["", "", ""] 16 | mock_file.return_value = audio 17 | 18 | # when 19 | data_crawler.crawl_one_file("fake_filename") 20 | 21 | # then 22 | mock_search.assert_called_with("fake_filename") 23 | 24 | 25 | @patch(f"{TESTED_MODULE}.DataCrawler.search_by_title_and_artist") 26 | @patch(f"{TESTED_MODULE}.get_file_manager") 27 | def test_crawl_one_file__when_crawling_we_start_by_searching_by_title_and_artist_if_there_available( 28 | mock_file, mock_search 29 | ): 30 | # given 31 | data_crawler = DataCrawler() 32 | audio = Mock() 33 | audio.get_tag_research.return_value = ["title", "artist", "", "", ""] 34 | mock_file.return_value = audio 35 | 36 | # when 37 | data_crawler.crawl_one_file("fake_filename") 38 | 39 | # then 40 | mock_search.assert_called_with("fake_filename", ["title", "artist", "", "", ""]) 41 | 42 | 43 | @patch(f"{TESTED_MODULE}.DataCrawler.search_by_artist_and_name_file") 44 | @patch(f"{TESTED_MODULE}.get_file_manager") 45 | def test_crawl_one_file__when_crawling_we_start_by_searching_by_artist_and_filename_if_no_artist( 46 | mock_file, mock_search 47 | ): 48 | # given 49 | data_crawler = DataCrawler() 50 | audio = Mock() 51 | audio.get_tag_research.return_value = ["", "artist", "album", "", ""] 52 | mock_file.return_value = audio 53 | 54 | # when 55 | data_crawler.crawl_one_file("fake_filename") 56 | 57 | # then 58 | mock_search.assert_called_with("fake_filename", ["", "artist", "album", "", ""]) 59 | 60 | 61 | def test_get_tags__smash_title_and_track_name_for_multiple_values(): 62 | # given 63 | crawler = DataCrawler() 64 | crawler.tag_founds = { 65 | "fake_file.mp3": { 66 | "title": " ftitle", 67 | "artist": "fartist", 68 | "genre": "fgenre", 69 | "year": "fyear", 70 | "album": "falbum", 71 | "cover": "fcover", 72 | "track": "33", 73 | }, 74 | "fake_second_file.mp3": { 75 | "title": " ftitle", 76 | "artist": "fartist", 77 | "genre": "fgenre", 78 | "year": "fyear2", 79 | "album": "falbum2", 80 | "cover": "fcover2", 81 | "track": "33", 82 | }, 83 | } 84 | 85 | # when 86 | output = crawler.get_tags(["fake_file.mp3", "fake_second_file.mp3"]) 87 | 88 | # then 89 | assert output == { 90 | "title": "", 91 | "artist": "fartist", 92 | "genre": "fgenre", 93 | "year": "", 94 | "album": "", 95 | "cover": "", 96 | "track": "", 97 | } 98 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | from src.tools import reorder_data 2 | 3 | 4 | def test_reorder_data_is_working_with_musicbrainz_data(): 5 | # given 6 | 7 | music_brainz_data = { 8 | "recording-list": [ 9 | { 10 | "id": "944c97bc-5341-4834-acb4-73ac24f1602e", 11 | "ext:score": "100", 12 | "title": "Bunker Sweet Bunker", 13 | "length": "175626", 14 | "artist-credit": [ 15 | { 16 | "name": "Les Malpolis", 17 | "artist": { 18 | "id": "e32f569d-bcd8-4c29-95a1-b13bff72f9d5", 19 | "name": "Les Malpolis", 20 | "sort-name": "Malpolis, Les", 21 | }, 22 | } 23 | ], 24 | "release-list": [ 25 | { 26 | "id": "577f349b-528e-4366-b26e-ab583af99a49", 27 | "title": "Les Malpolis élargissent leur cible", 28 | "status": "Official", 29 | "release-group": { 30 | "id": "faa1080b-0878-391e-bfef-db18b6e5f3ed", 31 | "type": "Album", 32 | "title": "Les Malpolis élargissent leur cible", 33 | "primary-type": "Album", 34 | }, 35 | "date": "2001", 36 | "country": "FR", 37 | "release-event-list": [ 38 | { 39 | "date": "2001", 40 | "area": { 41 | "id": "08310658-51eb-3801-80de-5a0739207115", 42 | "name": "France", 43 | "sort-name": "France", 44 | "iso-3166-1-code-list": ["FR"], 45 | }, 46 | } 47 | ], 48 | "medium-list": [ 49 | { 50 | "position": "1", 51 | "format": "CD", 52 | "track-list": [ 53 | { 54 | "id": "d4f9c6d8-f616-3434-a9e3-7eab00abb2b6", 55 | "number": "7", 56 | "title": "Bunker Sweet Bunker", 57 | "length": "175626", 58 | "track_or_recording_length": "175626", 59 | } 60 | ], 61 | "track-count": 17, 62 | } 63 | ], 64 | "medium-track-count": 17, 65 | "medium-count": 1, 66 | } 67 | ], 68 | "artist-credit-phrase": "Les Malpolis", 69 | } 70 | ], 71 | "recording-count": 784, 72 | } 73 | 74 | # when 75 | output = reorder_data(music_brainz_data) 76 | 77 | # then 78 | assert output == { 79 | "title": "Bunker Sweet Bunker", 80 | "artist": "Les Malpolis", 81 | "genre": "", 82 | "cover": "", 83 | "album": "Les Malpolis élargissent leur cible", 84 | "track": "7", 85 | "year": "2001", 86 | } 87 | -------------------------------------------------------------------------------- /com.github.lachhebo.Gabtag.Devel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "com.github.lachhebo.Gabtag.Devel", 3 | "runtime" : "org.gnome.Platform", 4 | "runtime-version" : "49", 5 | "sdk" : "org.gnome.Sdk", 6 | "command" : "gabtag", 7 | "finish-args" : [ 8 | "--device=dri", 9 | "--share=network", 10 | "--share=ipc", 11 | "--socket=fallback-x11", 12 | "--socket=wayland" 13 | ], 14 | "cleanup" : [ 15 | "/include", 16 | "/lib/pkgconfig", 17 | "/man", 18 | "/share/doc", 19 | "/share/gtk-doc", 20 | "/share/man", 21 | "/share/pkgconfig", 22 | "*.la", 23 | "*.a" 24 | ], 25 | "modules" : [ 26 | { 27 | "name" : "python3-mutagen", 28 | "buildsystem" : "simple", 29 | "build-commands" : [ 30 | "pip3 install --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} setuptools", 31 | "pip3 install --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} mutagen" 32 | ], 33 | "sources" : [ 34 | { 35 | "type": "file", 36 | "url": "https://files.pythonhosted.org/packages/source/s/setuptools/setuptools-75.0.0.tar.gz", 37 | "sha256": "25af69c809d9334cd8e653d385277abeb5a102dca255954005a7092d282575ea" 38 | }, 39 | { 40 | "type" : "file", 41 | "url" : "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", 42 | "sha256" : "719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99" 43 | } 44 | ] 45 | }, 46 | { 47 | "name" : "python3-Pillow", 48 | "buildsystem" : "simple", 49 | "build-commands" : [ 50 | "pip3 install --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} Pillow" 51 | ], 52 | "sources" : [ 53 | { 54 | "type" : "file", 55 | "url" : "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", 56 | "sha256" : "a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07" 57 | } 58 | ] 59 | }, 60 | { 61 | "name" : "python3-musicbrainzngs", 62 | "buildsystem" : "simple", 63 | "build-commands" : [ 64 | "pip3 install --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} musicbrainzngs" 65 | ], 66 | "sources" : [ 67 | { 68 | "type" : "file", 69 | "url" : "https://files.pythonhosted.org/packages/6d/fd/cef7b2580436910ccd2f8d3deec0f3c81743e15c0eb5b97dde3fbf33c0c8/musicbrainzngs-0.7.1-py2.py3-none-any.whl", 70 | "sha256" : "e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10" 71 | } 72 | ] 73 | }, 74 | { 75 | "name" : "gabtag", 76 | "buildsystem" : "meson", 77 | "builddir" : true, 78 | "config-opts" : [ 79 | "-Ddevel=true", 80 | "--libdir=lib" 81 | ], 82 | "sources" : [ 83 | { 84 | "type" : "dir", 85 | "path" : "." 86 | } 87 | ] 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /src/crawler_data.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | import musicbrainzngs as mb 4 | 5 | # from .audio_getter import get_file_manager 6 | from .tools import remove_extension, reorder_data 7 | from .version import __version__ 8 | 9 | 10 | class DataCrawler: 11 | def __init__(self): 12 | mb.set_useragent( 13 | "GabTag", version=__version__, contact="ismael.lachheb@protonmail.com" 14 | ) 15 | self.tag_founds = {} 16 | 17 | def crawl_one_file(self, name_file, directory): 18 | # audio = get_file_manager(name_file, directory) 19 | 20 | # tags = audio.get_tag_research() 21 | 22 | # if tags[0] == "" and tags[1] == "": 23 | self.search_by_filename(name_file) 24 | # elif tags[0] != "" and tags[1] != "": 25 | # self.search_by_title_and_artist(name_file, tags) 26 | # elif tags[1] == "": 27 | # self.search_by_title_and_album(name_file, tags) 28 | # elif tags[0] == "": 29 | # self.search_by_artist_and_name_file(name_file, tags) 30 | 31 | def search_by_artist_and_name_file(self, name_file: str, tags: List): 32 | try: 33 | gathered_data = mb.search_recordings( 34 | query=remove_extension(name_file), 35 | artistname=tags[1], 36 | limit=1, 37 | ) 38 | reordered_data = reorder_data(gathered_data) 39 | self.tag_founds[name_file] = reordered_data 40 | except mb.NetworkError: 41 | pass 42 | 43 | def search_by_title_and_album(self, name_file: str, tags): 44 | try: 45 | records = mb.search_recordings(recording=tags[0], release=tags[2], limit=1) 46 | records = reorder_data(records) 47 | self.tag_founds[name_file] = records 48 | except mb.NetworkError: 49 | pass 50 | 51 | def search_by_title_and_artist(self, name_file: str, tags): 52 | try: 53 | gathered_data = mb.search_recordings( 54 | recording=tags[0], artistname=tags[1], limit=1 55 | ) 56 | self.tag_founds[name_file] = reorder_data(gathered_data) 57 | except mb.NetworkError: 58 | pass 59 | 60 | def search_by_filename(self, name_file: str): 61 | mz_query = remove_extension(name_file) 62 | self.tag_founds[name_file] = reorder_data( 63 | mb.search_recordings(query=mz_query, limit=1) 64 | ) 65 | 66 | def update_data_crawled(self, modifications: Dict, directory: str) -> List: 67 | names_file = [] 68 | for name_file in modifications: 69 | self.crawl_one_file(name_file, directory) 70 | names_file.append(name_file) 71 | return names_file 72 | 73 | def erase_data(self): 74 | self.tag_founds = {} 75 | 76 | def get_data_from_online(self, file_list, directory: str): 77 | for name_file in file_list: 78 | self.crawl_one_file(name_file, directory) 79 | 80 | def get_tags(self, names_files): 81 | 82 | tags_output = None 83 | 84 | name_file = names_files[0] 85 | if name_file in self.tag_founds: 86 | tags_output = self.tag_founds[name_file].copy() 87 | 88 | if len(names_files) > 1: 89 | for name_file in names_files: 90 | if name_file not in self.tag_founds: 91 | return None 92 | _tags = ["artist", "album", "year", "genre", "cover"] 93 | for tag_iterator in _tags: 94 | tag_test = self.tag_founds[name_file][tag_iterator] 95 | if tags_output[tag_iterator] != tag_test: 96 | tags_output[tag_iterator] = "" 97 | tags_output["title"] = "" 98 | tags_output["track"] = "" 99 | 100 | return tags_output 101 | 102 | 103 | DATA_CRAWLER = DataCrawler() 104 | -------------------------------------------------------------------------------- /tests/test_view.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | from src.view import View 4 | 5 | 6 | def test__erase__set_text_to_empty_string_for_each_entry(): 7 | # given 8 | test_view = View() 9 | test_view.title = Mock() 10 | test_view.album = Mock() 11 | test_view.artist = Mock() 12 | test_view.genre = Mock() 13 | test_view.track = Mock() 14 | test_view.year = Mock() 15 | test_view.show_mbz = Mock() 16 | test_view.cover = Mock() 17 | 18 | # when 19 | test_view.erase() 20 | 21 | # then 22 | test_view.title.set_text.assert_called_with("") 23 | test_view.album.set_text.assert_called_with("") 24 | test_view.artist.set_text.assert_called_with("") 25 | test_view.genre.set_text.assert_called_with("") 26 | test_view.track.set_text.assert_called_with("") 27 | test_view.year.set_text.assert_called_with("") 28 | test_view.cover.set_from_icon_name.assert_called() 29 | test_view.show_mbz.assert_called_with( 30 | { 31 | "title": "", 32 | "track": "", 33 | "album": "", 34 | "genre": "", 35 | "artist": "", 36 | "cover": "", 37 | "year": "", 38 | } 39 | ) 40 | 41 | 42 | def test_show_tags__apply_all_tags_correctly(): 43 | # given 44 | test_view = View() 45 | test_view.title = Mock() 46 | test_view.album = Mock() 47 | test_view.artist = Mock() 48 | test_view.genre = Mock() 49 | test_view.track = Mock() 50 | test_view.year = Mock() 51 | test_view.show_mbz = Mock() 52 | 53 | test_view.length = Mock() 54 | test_view.size = Mock() 55 | 56 | test_view.cover = Mock() 57 | tags_dict = { 58 | "title": "fake_title", 59 | "track": "1", 60 | "album": "fake_album", 61 | "artist": "fake_artist", 62 | "length": "fake_length", 63 | "cover": "", 64 | "genre": "fake_genre", 65 | "size": "33", 66 | "year": "2021", 67 | } 68 | 69 | # when 70 | test_view.show_tags(tags_dict, 0) 71 | 72 | # then 73 | test_view.length.set_text.assert_called_with("fake_length") 74 | test_view.size.set_text.assert_called_with("33") 75 | 76 | test_view.genre.set_text.assert_called_with("fake_genre") 77 | test_view.album.set_text.assert_called_with("fake_album") 78 | test_view.artist.set_text.assert_called_with("fake_artist") 79 | test_view.year.set_text.assert_called_with("2021") 80 | 81 | test_view.title.set_text.assert_called_with("fake_title") 82 | test_view.title.set_editable.assert_called_with(1) 83 | 84 | test_view.track.set_text.assert_called_with("1") 85 | test_view.track.set_editable.assert_called_with(1) 86 | 87 | 88 | def test_show_tags__apply_all_tags_correctly_if_multiple_tag(): 89 | # given 90 | test_view = View() 91 | test_view.title = Mock() 92 | test_view.album = Mock() 93 | test_view.artist = Mock() 94 | test_view.genre = Mock() 95 | test_view.track = Mock() 96 | test_view.year = Mock() 97 | test_view.show_mbz = Mock() 98 | 99 | test_view.length = Mock() 100 | test_view.size = Mock() 101 | 102 | test_view.cover = Mock() 103 | tags_dict = { 104 | "title": "fake_title", 105 | "track": "1", 106 | "album": "fake_album", 107 | "artist": "fake_artist", 108 | "length": "fake_length", 109 | "cover": "", 110 | "genre": "fake_genre", 111 | "size": "33", 112 | "year": "2021", 113 | } 114 | 115 | # when 116 | test_view.show_tags(tags_dict, 1) 117 | 118 | # then 119 | test_view.length.set_text.assert_called_with("") 120 | test_view.size.set_text.assert_called_with("") 121 | 122 | test_view.genre.set_text.assert_called_with("fake_genre") 123 | test_view.album.set_text.assert_called_with("fake_album") 124 | test_view.artist.set_text.assert_called_with("fake_artist") 125 | test_view.year.set_text.assert_called_with("2021") 126 | 127 | test_view.title.set_text.assert_called_with("") 128 | test_view.title.set_editable.assert_called_with(0) 129 | 130 | test_view.track.set_text.assert_called_with("") 131 | test_view.track.set_editable.assert_called_with(0) 132 | -------------------------------------------------------------------------------- /src/audio_ogg_file_handler.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import mutagen.flac 3 | from typing import Dict 4 | from mutagen.flac import Picture 5 | from mutagen.id3 import ID3, TIT2, APIC, TALB, TPE1 # noqa:F401 6 | from mutagen.id3 import TCON, TRCK, TDRC, USLT # noqa:F401 7 | from mutagen.oggvorbis import OggVorbis, OggVCommentDict 8 | 9 | from .audio_extension_handler import AudioExtensionHandler 10 | from .tools import file_size_to_string 11 | from .tools import music_length_to_string 12 | 13 | TAG_PARAMS = { 14 | "title": "TITLE", 15 | "cover": "METADATA_BLOCK_PICTURE", 16 | "album": "ALBUM", 17 | "artist": "ARTIST", 18 | "genre": "GENRE", 19 | "track": "TRACKNUMBER", 20 | "year": "DATE", 21 | } 22 | 23 | 24 | class OggFileHandler(AudioExtensionHandler): 25 | @staticmethod 26 | def get_extension(): 27 | return ".ogg" 28 | 29 | def __init__(self, path_file): 30 | """ 31 | We initialise the path of the file and the tagging tool we use 32 | """ 33 | self.path_file = path_file 34 | self.audio = OggVorbis(path_file) 35 | self.id3 = self.audio.tags 36 | 37 | if self.id3 is None: 38 | self.audio.add_tags() 39 | self.id3 = self.audio.tags 40 | 41 | def get_one_tag(self, id3_name_tag: str, data_type: str): 42 | """ 43 | A function to return the first tag of an id3 label 44 | """ 45 | if self.id3 is None: 46 | return "" 47 | 48 | if self.id3 is OggVCommentDict: 49 | return self.id3.get(id3_name_tag) 50 | 51 | tag_needed = self.id3.get(id3_name_tag, "") 52 | 53 | if len(tag_needed) == 0: 54 | return "" 55 | 56 | if data_type == "text": 57 | return tag_needed[0] 58 | elif data_type == "data": 59 | try: 60 | return base64.b64decode(tag_needed[0]) 61 | except (TypeError, ValueError): 62 | return "" 63 | else: 64 | return "" 65 | 66 | def get_tag_research(self): 67 | return [ 68 | self.get_one_tag(TAG_PARAMS["title"], "text"), 69 | self.get_one_tag(TAG_PARAMS["artist"], "text"), 70 | self.get_one_tag(TAG_PARAMS["album"], "text"), 71 | ] 72 | 73 | def get_tag(self, tag_key): 74 | """ 75 | We handle tag using a switch, it is working well because it 76 | is basically the structure. 77 | """ 78 | 79 | if tag_key == "cover": 80 | cover_bytes = self.get_one_tag("METADATA_BLOCK_PICTURE", "data") 81 | if cover_bytes == "": 82 | return "" 83 | try: 84 | picture = Picture(cover_bytes) 85 | return picture.data 86 | except mutagen.flac.error: 87 | return "" 88 | elif tag_key == "size": 89 | return file_size_to_string(self.path_file) 90 | elif tag_key == "length": 91 | return music_length_to_string(self.audio.info.length) 92 | else: 93 | return self.get_one_tag(TAG_PARAMS[tag_key], "text") 94 | 95 | def get_tags(self) -> Dict: 96 | tags = [ 97 | "title", 98 | "album", 99 | "artist", 100 | "genre", 101 | "cover", 102 | "year", 103 | "track", 104 | "length", 105 | "size", 106 | ] 107 | result = {} 108 | for tag in tags: 109 | result[tag] = self.get_tag(tag) 110 | return result 111 | 112 | def check_tag_existence(self, key): 113 | return len(self.id3.get(TAG_PARAMS[key])) > 0 114 | 115 | def set_tag(self, tag_key, tag_value): 116 | 117 | if tag_key != "cover": 118 | self.id3[TAG_PARAMS[tag_key]] = [tag_value] 119 | return 1 120 | 121 | if tag_value == "": 122 | return 0 123 | 124 | if isinstance(tag_value, str): 125 | tag_value = open(tag_value, "rb").read() 126 | picture = Picture() 127 | picture.data = tag_value 128 | binary_data = picture.write() 129 | self.id3[TAG_PARAMS[tag_key]] = [base64.b64encode(binary_data).decode("ascii")] 130 | 131 | def save_modifications(self): 132 | """ 133 | Save definitively the modification we have made. 134 | """ 135 | self.audio.save() 136 | -------------------------------------------------------------------------------- /po/gabtag.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the gabtag package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: gabtag\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-04-12 16:48+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: data/com.github.lachhebo.Gabtag.desktop.in:3 21 | msgid "Gabtag" 22 | msgstr "" 23 | 24 | #: data/com.github.lachhebo.Gabtag.desktop.in:4 25 | msgid "Modify audio tags" 26 | msgstr "" 27 | 28 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:4 29 | msgid "GabTag" 30 | msgstr "" 31 | 32 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:13 33 | msgid "An audio tagging tool" 34 | msgstr "" 35 | 36 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:15 37 | msgid "" 38 | "GabTag is a Linux audio tagging tool written in GTK 4 and Adwaita, which " 39 | "makes it very suitable for GTK based desktop users." 40 | msgstr "" 41 | 42 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:19 43 | msgid "" 44 | "It allows users to select several files and modify their tags. It is also " 45 | "possible to let GabTag automatically find tags for an audio file using " 46 | "MusicBrainz." 47 | msgstr "" 48 | 49 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:33 50 | msgid "Ismaïl Lachheb" 51 | msgstr "" 52 | 53 | #: src/event_machine.py:52 src/window.ui:66 54 | msgid "Select a Folder" 55 | msgstr "" 56 | 57 | #: src/event_machine.py:111 58 | msgid "Open a File" 59 | msgstr "" 60 | 61 | #: src/tools.py:104 62 | msgid " minutes " 63 | msgstr "" 64 | 65 | #: src/tools.py:104 66 | msgid " seconds" 67 | msgstr "" 68 | 69 | #: src/tools.py:111 70 | msgid "All Supported Images" 71 | msgstr "" 72 | 73 | #: src/tools.py:117 74 | msgid "PNG Images" 75 | msgstr "" 76 | 77 | #: src/tools.py:122 78 | msgid "JPEG Images" 79 | msgstr "" 80 | 81 | #: src/treeview.py:24 82 | msgid "Name" 83 | msgstr "" 84 | 85 | #: src/treeview.py:29 86 | msgid "Data" 87 | msgstr "" 88 | 89 | #: src/treeview.py:44 src/treeview.py:63 90 | msgid "No" 91 | msgstr "" 92 | 93 | #: src/treeview.py:61 94 | msgid "Yes" 95 | msgstr "" 96 | 97 | #: src/window.ui:6 98 | msgid "Reset Files" 99 | msgstr "" 100 | 101 | #: src/window.ui:10 102 | msgid "Set Online Tags" 103 | msgstr "" 104 | 105 | #: src/window.ui:16 106 | msgid "About GabTag" 107 | msgstr "" 108 | 109 | #: src/window.ui:26 110 | msgid "Audio tagging tool." 111 | msgstr "" 112 | 113 | #. TRANSLATORS: 'Name ' or 'Name https://website.example' 114 | #: src/window.ui:35 115 | msgid "translator-credits" 116 | msgstr "" 117 | 118 | #: src/window.ui:64 119 | msgid "_Open" 120 | msgstr "" 121 | 122 | #: src/window.ui:76 123 | msgid "Main Menu" 124 | msgstr "" 125 | 126 | #: src/window.ui:82 127 | msgid "Save All" 128 | msgstr "" 129 | 130 | #: src/window.ui:130 131 | msgid "Load a Cover" 132 | msgstr "" 133 | 134 | #: src/window.ui:146 135 | msgid "Cover" 136 | msgstr "" 137 | 138 | #: src/window.ui:157 src/window.ui:262 139 | msgid "Title" 140 | msgstr "" 141 | 142 | #: src/window.ui:162 src/window.ui:271 143 | msgid "Album" 144 | msgstr "" 145 | 146 | #: src/window.ui:168 src/window.ui:279 147 | msgid "Artist" 148 | msgstr "" 149 | 150 | #: src/window.ui:174 src/window.ui:287 151 | msgid "Genre" 152 | msgstr "" 153 | 154 | #: src/window.ui:180 src/window.ui:295 155 | msgid "Track" 156 | msgstr "" 157 | 158 | #: src/window.ui:186 src/window.ui:303 159 | msgid "Year" 160 | msgstr "" 161 | 162 | #: src/window.ui:193 163 | msgid "Length" 164 | msgstr "" 165 | 166 | #: src/window.ui:204 167 | msgid "Size" 168 | msgstr "" 169 | 170 | #: src/window.ui:220 171 | msgid "Reset" 172 | msgstr "" 173 | 174 | #: src/window.ui:221 175 | msgid "Remove Changes in Fields" 176 | msgstr "" 177 | 178 | #: src/window.ui:230 179 | msgid "Save" 180 | msgstr "" 181 | 182 | #: src/window.ui:231 183 | msgid "Store Changes to File" 184 | msgstr "" 185 | 186 | #: src/window.ui:242 187 | msgid "MusicBrainz Tags" 188 | msgstr "" 189 | 190 | #: src/window.ui:313 191 | msgid "Set Tags" 192 | msgstr "" 193 | 194 | #: src/window.ui:314 195 | msgid "Use MusicBrainz Tags" 196 | msgstr "" 197 | 198 | #: src/window_gtk.py:73 199 | msgid "Donate" 200 | msgstr "" 201 | -------------------------------------------------------------------------------- /src/audio_mp3_file_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from mutagen.id3 import ID3, TIT2, APIC, TALB, TPE1 # noqa:F401 4 | from mutagen.id3 import TCON, TRCK, TDRC, USLT # noqa:F401 5 | from mutagen.mp3 import MP3 6 | 7 | from .audio_extension_handler import AudioExtensionHandler 8 | from .tools import get_extension_image, music_length_to_string 9 | from .tools import file_size_to_string 10 | 11 | TAG_PARAMS = { 12 | "title": "TIT2", 13 | "cover": "APIC", 14 | "album": "TALB", 15 | "artist": "TPE1", 16 | "genre": "TCON", 17 | "track": "TRCK", 18 | "year": "TDRC", 19 | } 20 | 21 | 22 | class Mp3FileHandler(AudioExtensionHandler): 23 | """ 24 | This function treat MP3 tags, There is no reference of MP3 in the 25 | code elsewhere, to handle a new file type, implement a similar class 26 | who is the children of AudioBasics. 27 | """ 28 | 29 | @staticmethod 30 | def get_extension(): 31 | return ".mp3" 32 | 33 | def __init__(self, path_file): 34 | """ 35 | We initialise the path of the file and the tagging tool we use 36 | """ 37 | self.path_file = path_file 38 | self.audio = MP3(path_file) 39 | self.id3 = self.audio.tags 40 | 41 | if self.id3 is None: 42 | self.audio.tags = ID3() 43 | self.id3 = self.audio.tags 44 | 45 | def get_one_tag(self, id3_name_tag: str, data_type: str) -> str: 46 | """ 47 | A function to return the first tag of an id3 label 48 | """ 49 | if self.id3 is None: 50 | return "" 51 | 52 | tag_needed = self.id3.getall(id3_name_tag) 53 | 54 | if len(tag_needed) == 0: 55 | return "" 56 | 57 | if data_type == "text": 58 | return tag_needed[0].text[0] 59 | elif data_type == "data": 60 | return tag_needed[0].data 61 | 62 | return "" 63 | 64 | def get_tag_research(self): 65 | return [ 66 | self.get_one_tag(TAG_PARAMS["title"], "text"), 67 | self.get_one_tag(TAG_PARAMS["artist"], "text"), 68 | self.get_one_tag(TAG_PARAMS["album"], "text"), 69 | ] 70 | 71 | def get_tag(self, tag_key): 72 | """ 73 | We handle tag using a switch, it is working well because it 74 | is basically the structure. 75 | """ 76 | 77 | if tag_key == "cover": 78 | return self.get_one_tag("APIC", "data") 79 | elif tag_key == "year": 80 | return str(self.get_one_tag("TDRC", "text")) 81 | elif tag_key == "size": 82 | return file_size_to_string(self.path_file) 83 | elif tag_key == "length": 84 | return music_length_to_string(self.audio.info.length) 85 | else: 86 | return self.get_one_tag(TAG_PARAMS[tag_key], "text") 87 | 88 | def get_tags(self) -> Dict: 89 | tags = [ 90 | "title", 91 | "album", 92 | "artist", 93 | "genre", 94 | "cover", 95 | "year", 96 | "track", 97 | "length", 98 | "size", 99 | ] 100 | result = {} 101 | for tag in tags: 102 | result[tag] = self.get_tag(tag) 103 | return result 104 | 105 | def check_tag_existence(self, key): 106 | return len(self.id3.getall(TAG_PARAMS[key])) > 0 107 | 108 | def set_tag(self, tag_key, tag_value): 109 | 110 | if tag_key != "cover": 111 | self.id3.delall(TAG_PARAMS[tag_key]) 112 | self.id3.add(globals()[TAG_PARAMS[tag_key]](encoding=3, text=tag_value)) 113 | return 1 114 | 115 | if tag_value == "": 116 | return 0 117 | 118 | self.id3.delall("APIC") 119 | if isinstance(tag_value, bytes): 120 | self.id3.add( 121 | APIC( 122 | encoding=3, # UTF-8 123 | mime="/image/png", # '/image/png' 124 | type=3, # 3 is for album art 125 | desc="Cover", 126 | data=tag_value, 127 | ) 128 | ) 129 | else: 130 | extension_image = get_extension_image(tag_value) 131 | self.id3.add( 132 | APIC( 133 | encoding=3, # UTF-8 134 | mime=extension_image, # '/image/png' 135 | type=3, # 3 is for album art 136 | desc="Cover", 137 | data=open(tag_value, "rb").read(), 138 | ) 139 | ) 140 | 141 | def save_modifications(self): 142 | """ 143 | Save definitively the modification we have made. 144 | """ 145 | self.id3.save(self.path_file) 146 | -------------------------------------------------------------------------------- /src/window_gtk.py: -------------------------------------------------------------------------------- 1 | from .event_machine import EVENT_MACHINE 2 | from .treeview import TREE_VIEW 3 | from .view import VIEW 4 | 5 | import gi 6 | import gettext 7 | 8 | gi.require_version("Gtk", "4.0") 9 | gi.require_version("Adw", "1") 10 | 11 | from gi.repository import Adw, Gio, GObject, Gtk # noqa: E402 12 | 13 | _ = gettext.gettext 14 | 15 | 16 | @Gtk.Template(resource_path="/com/github/lachhebo/Gabtag/window.ui") 17 | class GabtagWindow(Adw.ApplicationWindow): 18 | __gtype_name__ = "GabtagWindow" 19 | 20 | app_id = GObject.Property(type=str) 21 | version = GObject.Property(type=str) 22 | devel = GObject.Property(type=bool, default=False) 23 | 24 | # HeaderBar 25 | id_about_window = Gtk.Template.Child() 26 | 27 | # Table 28 | tree_view_id = Gtk.Template.Child() 29 | liststore1 = Gtk.Template.Child() 30 | 31 | # Tags 32 | id_album = Gtk.Template.Child() 33 | id_artist = Gtk.Template.Child() 34 | id_type = Gtk.Template.Child() 35 | id_title = Gtk.Template.Child() 36 | id_cover = Gtk.Template.Child() 37 | id_year = Gtk.Template.Child() 38 | id_track = Gtk.Template.Child() 39 | 40 | # Infos 41 | id_info_length = Gtk.Template.Child() 42 | id_info_size = Gtk.Template.Child() 43 | 44 | # MusicBrainz 45 | 46 | id_album_mbz = Gtk.Template.Child() 47 | id_artist_mbz = Gtk.Template.Child() 48 | id_genre_mbz = Gtk.Template.Child() 49 | id_title_mbz = Gtk.Template.Child() 50 | id_cover_mbz = Gtk.Template.Child() 51 | id_year_mbz = Gtk.Template.Child() 52 | id_track_mbz = Gtk.Template.Child() 53 | 54 | # Buttons 55 | 56 | but_open = Gtk.Template.Child() 57 | but_save = Gtk.Template.Child() 58 | id_load_cover = Gtk.Template.Child() 59 | id_reset_one = Gtk.Template.Child() 60 | id_save_one = Gtk.Template.Child() 61 | id_setmbz_but = Gtk.Template.Child() 62 | tree_selection_id = Gtk.Template.Child() 63 | 64 | def __init__(self, app_id, version, devel, **kwargs): 65 | super().__init__(**kwargs) 66 | 67 | if devel: 68 | self.add_css_class("devel") 69 | 70 | self.set_default_icon_name(app_id) 71 | self.id_about_window.set_application_icon(app_id) 72 | self.id_about_window.set_version(version) 73 | self.id_about_window.add_link(_("Donate"), "https://paypal.me/lachhebo") 74 | 75 | TREE_VIEW.store = self.liststore1 76 | TREE_VIEW.view = self.tree_view_id 77 | TREE_VIEW.add_columns() 78 | 79 | VIEW.tree_view = self.tree_view_id 80 | VIEW.title = self.id_title 81 | VIEW.album = self.id_album 82 | VIEW.artist = self.id_artist 83 | VIEW.genre = self.id_type 84 | VIEW.cover = self.id_cover 85 | VIEW.track = self.id_track 86 | VIEW.year = self.id_year 87 | VIEW.length = self.id_info_length 88 | VIEW.size = self.id_info_size 89 | VIEW.title_mbz = self.id_title_mbz 90 | VIEW.album_mbz = self.id_album_mbz 91 | VIEW.artist_mbz = self.id_artist_mbz 92 | VIEW.genre_mbz = self.id_genre_mbz 93 | VIEW.cover_mbz = self.id_cover_mbz 94 | VIEW.track_mbz = self.id_track_mbz 95 | VIEW.year_mbz = self.id_year_mbz 96 | 97 | # Connect Buttons 98 | 99 | self.but_open.connect("clicked", EVENT_MACHINE.on_open_clicked) 100 | self.but_save.connect("clicked", EVENT_MACHINE.on_but_saved_clicked) 101 | 102 | reset_all = Gio.SimpleAction.new("reset-all", None) 103 | reset_all.connect("activate", EVENT_MACHINE.on_reset_all_clicked) 104 | self.add_action(reset_all) 105 | 106 | set_online_tags = Gio.SimpleAction.new("set-online-tags", None) 107 | set_online_tags.connect("activate", EVENT_MACHINE.on_set_online_tags) 108 | self.add_action(set_online_tags) 109 | 110 | about = Gio.SimpleAction.new("about", None) 111 | about.connect("activate", EVENT_MACHINE.on_about_clicked) 112 | self.add_action(about) 113 | 114 | self.id_load_cover.connect("clicked", EVENT_MACHINE.on_load_cover_clicked) 115 | self.id_reset_one.connect("clicked", EVENT_MACHINE.on_reset_one_clicked) 116 | self.id_save_one.connect("clicked", EVENT_MACHINE.on_clicked_save_one) 117 | self.id_setmbz_but.connect("clicked", EVENT_MACHINE.on_set_mbz) 118 | self.tree_selection_id.connect("changed", EVENT_MACHINE.on_selected_changed) 119 | self.id_album.connect("changed", EVENT_MACHINE.on_album_changed) 120 | self.id_artist.connect("changed", EVENT_MACHINE.on_artist_changed) 121 | self.id_type.connect("changed", EVENT_MACHINE.on_type_changed) 122 | self.id_title.connect("changed", EVENT_MACHINE.on_title_changed) 123 | self.id_year.connect("changed", EVENT_MACHINE.on_year_changed) 124 | self.id_track.connect("changed", EVENT_MACHINE.on_track_changed) 125 | 126 | EVENT_MACHINE.window = self 127 | -------------------------------------------------------------------------------- /po/tr.po: -------------------------------------------------------------------------------- 1 | # Turkish translation for gabtag. 2 | # Copyright (C) 2024 gabtag's COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the gabtag package. 4 | # 5 | # Sabri Ünal , 2024. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: gabtag\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-04-12 16:48+0300\n" 12 | "PO-Revision-Date: 2024-04-12 16:55+0300\n" 13 | "Last-Translator: Sabri Ünal \n" 14 | "Language-Team: Turkish \n" 15 | "Language: tr\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | "X-Generator: Poedit 3.4.2\n" 21 | 22 | #: data/com.github.lachhebo.Gabtag.desktop.in:3 23 | msgid "Gabtag" 24 | msgstr "Gabtag" 25 | 26 | #: data/com.github.lachhebo.Gabtag.desktop.in:4 27 | msgid "Modify audio tags" 28 | msgstr "Ses etiketlerini değiştir" 29 | 30 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:4 31 | msgid "GabTag" 32 | msgstr "GabTag" 33 | 34 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:13 35 | msgid "An audio tagging tool" 36 | msgstr "Ses etiketleme aracı" 37 | 38 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:15 39 | msgid "" 40 | "GabTag is a Linux audio tagging tool written in GTK 4 and Adwaita, which " 41 | "makes it very suitable for GTK based desktop users." 42 | msgstr "" 43 | "GabTag, GTK 4 ve Adwaita ile yazılmış Linux ses etiketleme aracıdır. GTK " 44 | "tabanlı masaüstü kullanıcıları için idealdir." 45 | 46 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:19 47 | msgid "" 48 | "It allows users to select several files and modify their tags. It is also " 49 | "possible to let GabTag automatically find tags for an audio file using " 50 | "MusicBrainz." 51 | msgstr "" 52 | "Kullanıcıların dosya seçmesine ve etiketlerini değiştirmesini sağlar. " 53 | "GabTag, MusicBrainz kullanarak ses dosyası için etiketleri kendiliğinden " 54 | "de bulabilir." 55 | 56 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:33 57 | msgid "Ismaïl Lachheb" 58 | msgstr "Ismaïl Lachheb" 59 | 60 | #: src/event_machine.py:52 src/window.ui:66 61 | msgid "Select a Folder" 62 | msgstr "Klasör Seç" 63 | 64 | #: src/event_machine.py:111 65 | msgid "Open a File" 66 | msgstr "Dosya Aç" 67 | 68 | #: src/tools.py:104 69 | msgid " minutes " 70 | msgstr " dakika " 71 | 72 | #: src/tools.py:104 73 | msgid " seconds" 74 | msgstr " saniye" 75 | 76 | #: src/tools.py:111 77 | msgid "All Supported Images" 78 | msgstr "Desteklenen Tüm Görüntüler" 79 | 80 | #: src/tools.py:117 81 | msgid "PNG Images" 82 | msgstr "PNG Görüntüler" 83 | 84 | #: src/tools.py:122 85 | msgid "JPEG Images" 86 | msgstr "JPEG Görüntüler" 87 | 88 | #: src/treeview.py:24 89 | msgid "Name" 90 | msgstr "Ad" 91 | 92 | #: src/treeview.py:29 93 | msgid "Data" 94 | msgstr "Veri" 95 | 96 | #: src/treeview.py:44 src/treeview.py:63 97 | msgid "No" 98 | msgstr "Hayır" 99 | 100 | #: src/treeview.py:61 101 | msgid "Yes" 102 | msgstr "Evet" 103 | 104 | #: src/window.ui:6 105 | msgid "Reset Files" 106 | msgstr "Dosyaları Sıfırla" 107 | 108 | #: src/window.ui:10 109 | msgid "Set Online Tags" 110 | msgstr "Çevrim İçi Etiketleri Ayarla" 111 | 112 | #: src/window.ui:16 113 | msgid "About GabTag" 114 | msgstr "GabTag Hakkında" 115 | 116 | #: src/window.ui:26 117 | msgid "Audio tagging tool." 118 | msgstr "Ses etiketleme aracı." 119 | 120 | #. TRANSLATORS: 'Name ' or 'Name https://website.example' 121 | #: src/window.ui:35 122 | msgid "translator-credits" 123 | msgstr "Sabri Ünal " 124 | 125 | #: src/window.ui:64 126 | msgid "_Open" 127 | msgstr "_Aç" 128 | 129 | #: src/window.ui:76 130 | msgid "Main Menu" 131 | msgstr "Ana Menü" 132 | 133 | #: src/window.ui:82 134 | msgid "Save All" 135 | msgstr "Tümünü Kaydet" 136 | 137 | #: src/window.ui:130 138 | msgid "Load a Cover" 139 | msgstr "Kapak Yükle" 140 | 141 | #: src/window.ui:146 142 | msgid "Cover" 143 | msgstr "Kapak" 144 | 145 | #: src/window.ui:157 src/window.ui:262 146 | msgid "Title" 147 | msgstr "Başlık" 148 | 149 | #: src/window.ui:162 src/window.ui:271 150 | msgid "Album" 151 | msgstr "Albüm" 152 | 153 | #: src/window.ui:168 src/window.ui:279 154 | msgid "Artist" 155 | msgstr "Sanatçı" 156 | 157 | #: src/window.ui:174 src/window.ui:287 158 | msgid "Genre" 159 | msgstr "Tür" 160 | 161 | #: src/window.ui:180 src/window.ui:295 162 | msgid "Track" 163 | msgstr "Parça" 164 | 165 | #: src/window.ui:186 src/window.ui:303 166 | msgid "Year" 167 | msgstr "Yıl" 168 | 169 | #: src/window.ui:193 170 | msgid "Length" 171 | msgstr "Uzunluk" 172 | 173 | #: src/window.ui:204 174 | msgid "Size" 175 | msgstr "Boyut" 176 | 177 | #: src/window.ui:220 178 | msgid "Reset" 179 | msgstr "Sıfırla" 180 | 181 | #: src/window.ui:221 182 | msgid "Remove Changes in Fields" 183 | msgstr "Alanlardaki Değişiklikleri Kaldır" 184 | 185 | #: src/window.ui:230 186 | msgid "Save" 187 | msgstr "Kaydet" 188 | 189 | #: src/window.ui:231 190 | msgid "Store Changes to File" 191 | msgstr "Değişiklikleri Dosyada Sakla" 192 | 193 | #: src/window.ui:242 194 | msgid "MusicBrainz Tags" 195 | msgstr "MusicBrainz Etiketleri" 196 | 197 | #: src/window.ui:313 198 | msgid "Set Tags" 199 | msgstr "Etiketleri Ayarla" 200 | 201 | #: src/window.ui:314 202 | msgid "Use MusicBrainz Tags" 203 | msgstr "MusicBrainz Etiketlerini Kullan" 204 | 205 | #: src/window_gtk.py:73 206 | msgid "Donate" 207 | msgstr "Bağış Yap" 208 | -------------------------------------------------------------------------------- /src/tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List 3 | 4 | import gi 5 | import musicbrainzngs as mb 6 | 7 | import gettext 8 | 9 | from .extension_manager import is_extension_managed 10 | from .selection_handler import SELECTION 11 | 12 | gi.require_version("Gtk", "4.0") 13 | 14 | from gi.repository import Gio, Gtk # noqa: E402 15 | 16 | _ = gettext.gettext 17 | 18 | 19 | def remove_extension(filename: str): 20 | """ 21 | return the filename without the extension 22 | """ 23 | namelist = filename.split(".") 24 | return namelist[0:-1] 25 | 26 | 27 | def reorder_data(music_brainz_data: Dict): 28 | """ 29 | take a bunch of data from mz and make it in the form { title = , ...} 30 | """ 31 | 32 | file_tags = { 33 | "title": "", 34 | "artist": "", 35 | "genre": "", 36 | "cover": "", 37 | "album": "", 38 | "track": "", 39 | "year": "", 40 | } 41 | 42 | if len(music_brainz_data["recording-list"]) >= 1: 43 | 44 | recording_list = music_brainz_data["recording-list"][0] 45 | artist = recording_list["artist-credit"][0]["artist"] 46 | file_tags["title"] = recording_list["title"] 47 | file_tags["artist"] = artist["name"] 48 | 49 | if "disambiguation" in artist: 50 | file_tags["genre"] = artist["disambiguation"] 51 | else: 52 | file_tags["genre"] = "" 53 | 54 | if "release-list" in recording_list: 55 | for i in range(len(recording_list["release-list"])): 56 | try: 57 | 58 | file_tags["cover"] = mb.get_image( 59 | mbid=recording_list["release-list"][i]["id"], 60 | coverid="front", 61 | size=250, 62 | ) 63 | 64 | if isinstance(file_tags, bytes): 65 | break 66 | 67 | except mb.musicbrainz.ResponseError: 68 | file_tags["cover"] = "" 69 | 70 | # album 71 | release_list = recording_list["release-list"][0] 72 | track = release_list["medium-list"][0]["track-list"][0] 73 | file_tags["album"] = release_list["release-group"]["title"] 74 | file_tags["track"] = track["number"] 75 | 76 | if "date" in release_list: 77 | file_tags["year"] = release_list["date"].split("-")[0] 78 | else: 79 | file_tags["year"] = "" 80 | else: 81 | file_tags["album"] = "" 82 | file_tags["track"] = "" 83 | file_tags["year"] = "" 84 | file_tags["cover"] = "" 85 | 86 | return file_tags 87 | 88 | 89 | def get_extension_image(filename): 90 | """ 91 | return a mime from a filename 92 | """ 93 | namelist = filename.split(".") 94 | return "/image/" + namelist[-1] 95 | 96 | 97 | def file_size_to_string(path_file): 98 | return str(round(os.path.getsize(path_file) / 1000000, 1)) + " Mb" 99 | 100 | 101 | def music_length_to_string(length): 102 | minutes = str(int(length / 60)) 103 | seconds = str(int(length % 60)) 104 | return minutes + _(" minutes ") + seconds + _(" seconds") 105 | 106 | 107 | def add_filters(dialog): 108 | store = Gio.ListStore.new(Gtk.FileFilter) 109 | 110 | filter_all = Gtk.FileFilter() 111 | filter_all.set_name(_("All Supported Images")) 112 | filter_all.add_mime_type("image/png") 113 | filter_all.add_mime_type("image/jpeg") 114 | store.append(filter_all) 115 | 116 | filter_png = Gtk.FileFilter() 117 | filter_png.set_name(_("PNG Images")) 118 | filter_png.add_mime_type("image/png") 119 | store.append(filter_png) 120 | 121 | filter_jpeg = Gtk.FileFilter() 122 | filter_jpeg.set_name(_("JPEG Images")) 123 | filter_jpeg.add_mime_type("image/jpeg") 124 | store.append(filter_jpeg) 125 | 126 | dialog.set_filters(store) 127 | 128 | 129 | def set_label(view_label, multiple_rows, value): 130 | if multiple_rows == 1: 131 | view_label.set_text("") 132 | else: 133 | view_label.set_text(value) 134 | 135 | 136 | def set_text_widget_permission(text_widget, multiple_rows, value): 137 | if multiple_rows == 1: 138 | text_widget.set_text("") 139 | text_widget.set_editable(0) 140 | else: 141 | text_widget.set_editable(1) 142 | text_widget.set_text(value) 143 | 144 | 145 | def get_file_list(directory: str): 146 | file_list = [] 147 | for _, _, file_name in os.walk(directory): 148 | file_list.extend(file_name) 149 | break 150 | 151 | result = [] 152 | for name_file in file_list: 153 | if is_extension_managed(name_file): 154 | result.append(name_file) 155 | 156 | return result 157 | 158 | 159 | def is_selection_valid(default_file_names: List) -> bool: 160 | file_names = get_filenames_from_selection(SELECTION.selection) 161 | 162 | if len(file_names) != len(default_file_names): 163 | return False 164 | 165 | for name_file, selected_name_file in zip(default_file_names, file_names): 166 | if name_file != selected_name_file: 167 | return False 168 | 169 | return True 170 | 171 | 172 | def get_filenames_from_selection(selection): 173 | model, list_iter = selection.get_selected_rows() 174 | name_files = [model[list_iter[i]][0] for i in range(len(list_iter))] 175 | return name_files 176 | -------------------------------------------------------------------------------- /po/es.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the gabtag package. 4 | # MGMX , 2019 5 | # Óscar Fernández Díaz , 2022. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: gabtag\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-08-17 00:26+0200\n" 12 | "PO-Revision-Date: 2022-08-17 00:28+0200\n" 13 | "Last-Translator: Óscar Fernández Díaz \n" 14 | "Language-Team: \n" 15 | "Language: es\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 20 | "X-Generator: Gtranslator 42.0\n" 21 | 22 | #: data/com.github.lachhebo.Gabtag.desktop.in:3 23 | msgid "Gabtag" 24 | msgstr "Gabtag" 25 | 26 | #: data/com.github.lachhebo.Gabtag.desktop.in:4 27 | msgid "Modify audio tags" 28 | msgstr "Modificar etiquetas de audio" 29 | 30 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:13 31 | msgid "An audio tagging tool" 32 | msgstr "Una herramienta para etiquetar archivos de audio" 33 | 34 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:15 35 | msgid "" 36 | "GabTag is a Linux audio tagging tool written in GTK 3, which makes it very " 37 | "suitable for GTK based desktop users." 38 | msgstr "" 39 | "GabTag es una herramienta de etiquetado de audio para Linux escrita en GTK " 40 | "3, lo que la hace muy adecuada para usuarios de escritorio basados en GTK." 41 | 42 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:19 43 | msgid "" 44 | "It allows users to select several files and modify their tags. It is also " 45 | "possible to let GabTag automatically find tags and lyrics for an audio file " 46 | "using MusicBrainz and lyrics.wikia.com database." 47 | msgstr "" 48 | "Permite a los usuarios seleccionar varios archivos y modificar sus " 49 | "etiquetas. También es posible dejar que GabTag encuentre automáticamente las " 50 | "etiquetas y las letras de un archivo de audio usando la base de datos " 51 | "MusicBrainz y lyrics.wikia.com." 52 | 53 | #: src/event_machine.py:51 src/window.ui:61 54 | msgid "Select a Folder" 55 | msgstr "Seleccionar una carpeta" 56 | 57 | #: src/event_machine.py:55 src/event_machine.py:120 src/window.ui:59 58 | msgid "_Open" 59 | msgstr "_Abrir" 60 | 61 | #: src/event_machine.py:56 src/event_machine.py:121 62 | msgid "_Cancel" 63 | msgstr "_Cancelar" 64 | 65 | #: src/event_machine.py:116 66 | msgid "Open a File" 67 | msgstr "Abrir un archivo" 68 | 69 | #: src/tools.py:104 70 | msgid " minutes " 71 | msgstr " minutos " 72 | 73 | #: src/tools.py:104 74 | msgid " seconds" 75 | msgstr " segundos" 76 | 77 | #: src/tools.py:109 78 | msgid "All Supported Images" 79 | msgstr "Todas las imágenes soportadas" 80 | 81 | #: src/tools.py:115 82 | msgid "PNG Images" 83 | msgstr "Imágenes PNG" 84 | 85 | #: src/tools.py:120 86 | msgid "JPEG Images" 87 | msgstr "Imágenes JPEG" 88 | 89 | #: src/treeview.py:24 90 | msgid "Name" 91 | msgstr "Nombre" 92 | 93 | #: src/treeview.py:29 94 | msgid "Data" 95 | msgstr "Datos" 96 | 97 | #: src/treeview.py:44 src/treeview.py:63 98 | msgid "No" 99 | msgstr "No" 100 | 101 | #: src/treeview.py:61 102 | msgid "Yes" 103 | msgstr "Sí" 104 | 105 | #: src/window.ui:6 106 | msgid "Reset Files" 107 | msgstr "Restablecer archivos" 108 | 109 | #: src/window.ui:10 110 | msgid "Set Online Tags" 111 | msgstr "Establecer etiquetas en línea" 112 | 113 | #: src/window.ui:16 114 | msgid "About GabTag" 115 | msgstr "Acerca de GabTag" 116 | 117 | #: src/window.ui:26 118 | msgid "Audio tagging tool." 119 | msgstr "Herramienta de etiquetado de audio." 120 | 121 | #. TRANSLATORS: 'Name ' or 'Name https://website.example' 122 | #: src/window.ui:30 123 | msgid "translator-credits" 124 | msgstr "Óscar Fernández Díaz " 125 | 126 | #: src/window.ui:71 127 | msgid "Main Menu" 128 | msgstr "Menú principal" 129 | 130 | #: src/window.ui:77 131 | msgid "Save All" 132 | msgstr "Guardar todo" 133 | 134 | #: src/window.ui:125 135 | msgid "Load a Cover" 136 | msgstr "Cargar una portada" 137 | 138 | #: src/window.ui:141 139 | msgid "Cover" 140 | msgstr "Portada" 141 | 142 | #: src/window.ui:152 src/window.ui:257 143 | msgid "Title" 144 | msgstr "Título" 145 | 146 | #: src/window.ui:157 src/window.ui:266 147 | msgid "Album" 148 | msgstr "Álbum" 149 | 150 | #: src/window.ui:163 src/window.ui:274 151 | msgid "Artist" 152 | msgstr "Artista" 153 | 154 | #: src/window.ui:169 src/window.ui:282 155 | msgid "Genre" 156 | msgstr "Género" 157 | 158 | #: src/window.ui:175 src/window.ui:290 159 | msgid "Track" 160 | msgstr "Pista" 161 | 162 | #: src/window.ui:181 src/window.ui:298 163 | msgid "Year" 164 | msgstr "Año" 165 | 166 | #: src/window.ui:188 167 | msgid "Length" 168 | msgstr "Duración" 169 | 170 | #: src/window.ui:199 171 | msgid "Size" 172 | msgstr "Tamaño" 173 | 174 | #: src/window.ui:215 175 | msgid "Reset" 176 | msgstr "Restablecer" 177 | 178 | #: src/window.ui:216 179 | msgid "Remove Changes in Fields" 180 | msgstr "Quitar los cambios en los campos" 181 | 182 | #: src/window.ui:225 183 | msgid "Save" 184 | msgstr "Guardar" 185 | 186 | #: src/window.ui:226 187 | msgid "Store Changes to File" 188 | msgstr "Almacenar los cambios en el archivo" 189 | 190 | #: src/window.ui:237 191 | msgid "MusicBrainz Tags" 192 | msgstr "Etiquetas de MusicBrainz" 193 | 194 | #: src/window.ui:308 195 | msgid "Set Tags" 196 | msgstr "Establecer etiquetas" 197 | 198 | #: src/window.ui:309 199 | msgid "Use MusicBrainz Tags" 200 | msgstr "Usar las etiquetas de MusicBrainz" 201 | -------------------------------------------------------------------------------- /po/it.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the gabtag package. 4 | # Albano Battistella , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: gabtag\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-06-06 15:04-0400\n" 11 | "PO-Revision-Date: 2020-05-03 10:00+0200\n" 12 | "Last-Translator: Albano Battistella \n" 13 | "Language-Team: ITALIAN \n" 14 | "Language: it\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: data/com.github.lachhebo.Gabtag.desktop.in:3 20 | msgid "Gabtag" 21 | msgstr "Gabtag" 22 | 23 | #: data/com.github.lachhebo.Gabtag.desktop.in:4 24 | msgid "Modify audio tags" 25 | msgstr "Modifica i tag audio" 26 | 27 | #: data/com.github.lachhebo.Gabtag.desktop.in:5 28 | msgid "com.github.lachhebo.Gabtag" 29 | msgstr "com.github.lachhebo.Gabtag" 30 | 31 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:4 32 | msgid "GabTag" 33 | msgstr "GabTag" 34 | 35 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:14 36 | msgid "An audio tagging tool" 37 | msgstr "Uno strumento di codifica audio" 38 | 39 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:16 40 | msgid "" 41 | "GabTag is a Linux audio tagging tool written in GTK 3, which makes it very " 42 | "suitable for gtk based desktop users." 43 | msgstr "" 44 | "GabTag è uno strumento di tagging audio Linux scritto in GTK 3, che lo rende molto" 45 | "adatto per utenti desktop basati su gtk." 46 | 47 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:20 48 | msgid "" 49 | "It allow users to select several file and modify their tags, It is also " 50 | "possible to directly let GabTag automatically find tags and lyrics for the " 51 | "audio files using online data." 52 | msgstr "" 53 | "Permette agli utenti di selezionare più file e modificare i loro tag, è anche" 54 | "possibile lasciare che GabTag trovi automaticamente tag e testi per il" 55 | "file audio che utilizzano dati online." 56 | 57 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:24 58 | msgid "" 59 | "GabTag is written in Python 3, uses Mutagen to handle audio tag. It also " 60 | "uses MusicBrainz database to find audio tags and lyrics.wikia.com to find " 61 | "lyrics online." 62 | msgstr "" 63 | "GabTag è scritto in Python 3, usa Mutagen per gestire i tag audio. Inoltre" 64 | "utilizza il database MusicBrainz per trovare tag audio e lyrics.wikia.com per trovare" 65 | "testi online." 66 | 67 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:38 68 | msgid "Ismaïl Lachheb" 69 | msgstr "Ismaïl Lachheb" 70 | 71 | #: src/window.ui:17 src/window.ui:97 72 | msgid "About" 73 | msgstr "Inoformazioni su" 74 | 75 | #: src/window.ui:21 76 | msgid "gabtag is an open-source audio tagging tool written in gtk3." 77 | msgstr "gabtag is an open-source audio tagging tool written in gtk3." 78 | 79 | #: src/window.ui:69 80 | msgid "Reset files" 81 | msgstr "Ripristina file" 82 | 83 | #: src/window.ui:83 84 | msgid "Set online tags" 85 | msgstr "Imposta tag online" 86 | 87 | #: src/window.ui:132 88 | msgid "Add a Folder to modify tags" 89 | msgstr "Aggiungi una cartella per modificare i tag" 90 | 91 | #: src/window.ui:134 92 | msgid "Open" 93 | msgstr "Apri" 94 | 95 | #: src/window.ui:167 96 | msgid "Save All" 97 | msgstr "Salva Tutto" 98 | 99 | #: src/window.ui:211 100 | msgid "Name" 101 | msgstr "Nome" 102 | 103 | #: src/window.ui:252 104 | msgid "Album" 105 | msgstr "Album" 106 | 107 | #: src/window.ui:270 108 | msgid "Artist" 109 | msgstr "Artista" 110 | 111 | #: src/window.ui:288 112 | msgid "Genre" 113 | msgstr "Genere" 114 | 115 | #: src/window.ui:306 116 | msgid "Title" 117 | msgstr "Titolo" 118 | 119 | #: src/window.ui:394 120 | msgid "Cover" 121 | msgstr "Cover" 122 | 123 | #: src/window.ui:406 124 | msgid "Load" 125 | msgstr "Carica" 126 | 127 | #: src/window.ui:410 128 | msgid "Load a cover (jpeg or png)" 129 | msgstr "Carica una cover (jpeg o png)" 130 | 131 | #: src/window.ui:453 132 | msgid "Track number" 133 | msgstr "Numero traccia" 134 | 135 | #: src/window.ui:471 136 | msgid "Year" 137 | msgstr "Anno" 138 | 139 | #: src/window.ui:523 140 | msgid "Length" 141 | msgstr "Lunghezza" 142 | 143 | #: src/window.ui:538 144 | msgid "Size" 145 | msgstr "Dimensione" 146 | 147 | #: src/window.ui:575 148 | msgid "Reset Modification" 149 | msgstr "Ripristina modifiche" 150 | 151 | #: src/window.ui:579 152 | msgid "Remove modification made to fields." 153 | msgstr "Rimuovi le modifiche apportate ai campi." 154 | 155 | #: src/window.ui:593 156 | msgid "Save Modification" 157 | msgstr "Salva modifiche" 158 | 159 | #: src/window.ui:598 160 | msgid "Save the modification made to this file." 161 | msgstr "Salva la modifica apportata a questo file." 162 | 163 | #: src/window.ui:619 164 | msgid "MusicBrainz Tags" 165 | msgstr "Tag MusicBrainz" 166 | 167 | #: src/window.ui:637 168 | msgid "Title " 169 | msgstr "Titolo " 170 | 171 | #: src/window.ui:658 172 | msgid "Album " 173 | msgstr "Album" 174 | 175 | #: src/window.ui:678 176 | msgid "Artist " 177 | msgstr "Artista " 178 | 179 | #: src/window.ui:698 180 | msgid "Genre " 181 | msgstr "Genere " 182 | 183 | #: src/window.ui:718 184 | msgid "Track " 185 | msgstr "Traccia " 186 | 187 | #: src/window.ui:738 188 | msgid "Year " 189 | msgstr "Anno " 190 | 191 | #: src/window.ui:753 192 | msgid "Set Tags" 193 | msgstr "Imposta tag" 194 | 195 | #: src/window.ui:757 196 | msgid "Use the MusicBrainz tags" 197 | msgstr "Usa i tag MusicBrainz" 198 | 199 | #: src/window.ui:789 200 | msgid "Lyrics " 201 | msgstr "Testi" 202 | 203 | #: src/window.ui:814 204 | msgid "Set Lyrics" 205 | msgstr "Imposta testi" 206 | -------------------------------------------------------------------------------- /po/sv.po: -------------------------------------------------------------------------------- 1 | # Swedish translation for GabTag. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the gabtag package. 4 | # Åke Engelbrektson , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: gabtag\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2019-06-06 15:04-0400\n" 11 | "PO-Revision-Date: 2020-05-02 14:43+0200\n" 12 | "Language-Team: \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "X-Generator: Poedit 2.0.6\n" 17 | "Last-Translator: Åke Engelbrektson \n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "Language: sv_SE\n" 20 | 21 | #: data/com.github.lachhebo.Gabtag.desktop.in:3 22 | msgid "Gabtag" 23 | msgstr "Gabtag" 24 | 25 | #: data/com.github.lachhebo.Gabtag.desktop.in:4 26 | msgid "Modify audio tags" 27 | msgstr "Ändra ljudtaggar" 28 | 29 | #: data/com.github.lachhebo.Gabtag.desktop.in:5 30 | msgid "com.github.lachhebo.Gabtag" 31 | msgstr "com.github.lachhebo.Gabtag" 32 | 33 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:4 34 | msgid "GabTag" 35 | msgstr "GabTag" 36 | 37 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:14 38 | msgid "An audio tagging tool" 39 | msgstr "Ett ljudtaggningsverktyg" 40 | 41 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:16 42 | msgid "" 43 | "GabTag is a Linux audio tagging tool written in GTK 3, which makes it very " 44 | "suitable for gtk based desktop users." 45 | msgstr "" 46 | "GabTag är ett ljudtaggningsverktyg för Linux, skrivet i GTK 3, vilket är " 47 | "mycket passande för användare av GTK-baserade skrivbord." 48 | 49 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:20 50 | msgid "" 51 | "It allow users to select several file and modify their tags, It is also " 52 | "possible to directly let GabTag automatically find tags and lyrics for the " 53 | "audio files using online data." 54 | msgstr "" 55 | "Det låter användare välja ett flertal filer och tagga om dem. Det är också " 56 | "möjligt att låta GabTag automatiskt hitta taggar och låttexter för " 57 | "ljudfilerna, med online-data." 58 | 59 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:24 60 | msgid "" 61 | "GabTag is written in Python 3, uses Mutagen to handle audio tag. It also " 62 | "uses MusicBrainz database to find audio tags and lyrics.wikia.com to find " 63 | "lyrics online." 64 | msgstr "" 65 | "GabTag är skrivet i Python 3, använder Mutagen för att hantera ljudtaggar. " 66 | "Det använder också MusicBrainz databas för att hitta ljudtaggar och lyrics." 67 | "wikia.com för att hitta låttexter på nätet." 68 | 69 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:38 70 | msgid "Ismaïl Lachheb" 71 | msgstr "Ismaïl Lachheb" 72 | 73 | #: src/window.ui:17 src/window.ui:97 74 | msgid "About" 75 | msgstr "Om" 76 | 77 | #: src/window.ui:21 78 | msgid "gabtag is an open-source audio tagging tool written in gtk3." 79 | msgstr "" 80 | "GabTag är ett öppen-källkodsverktyg för taggning av ljud, skrivet i GTK3." 81 | 82 | #: src/window.ui:69 83 | msgid "Reset files" 84 | msgstr "Återställ filerna" 85 | 86 | #: src/window.ui:83 87 | msgid "Set online tags" 88 | msgstr "Använd online-taggar" 89 | 90 | #: src/window.ui:132 91 | msgid "Add a Folder to modify tags" 92 | msgstr "Lägg till en mapp för att ändra taggar" 93 | 94 | #: src/window.ui:134 95 | msgid "Open" 96 | msgstr "Öppna" 97 | 98 | #: src/window.ui:167 99 | msgid "Save All" 100 | msgstr "Spara alla" 101 | 102 | #: src/window.ui:211 103 | msgid "Name" 104 | msgstr "Namn" 105 | 106 | #: src/window.ui:252 107 | msgid "Album" 108 | msgstr "Album" 109 | 110 | #: src/window.ui:270 111 | msgid "Artist" 112 | msgstr "Artist" 113 | 114 | #: src/window.ui:288 115 | msgid "Genre" 116 | msgstr "Genre" 117 | 118 | #: src/window.ui:306 119 | msgid "Title" 120 | msgstr "Titel" 121 | 122 | #: src/window.ui:394 123 | msgid "Cover" 124 | msgstr "Omslag" 125 | 126 | #: src/window.ui:406 127 | msgid "Load" 128 | msgstr "Läs in" 129 | 130 | #: src/window.ui:410 131 | msgid "Load a cover (jpeg or png)" 132 | msgstr "Läs in ett omslag (jpeg eller png)" 133 | 134 | #: src/window.ui:453 135 | msgid "Track number" 136 | msgstr "Spårnummer" 137 | 138 | #: src/window.ui:471 139 | msgid "Year" 140 | msgstr "År" 141 | 142 | #: src/window.ui:523 143 | msgid "Length" 144 | msgstr "Längd" 145 | 146 | #: src/window.ui:538 147 | msgid "Size" 148 | msgstr "Storlek" 149 | 150 | #: src/window.ui:575 151 | msgid "Reset Modification" 152 | msgstr "Återställ ändringar" 153 | 154 | #: src/window.ui:579 155 | msgid "Remove modification made to fields." 156 | msgstr "Ta bort gjorda ändringar i fälten." 157 | 158 | #: src/window.ui:593 159 | msgid "Save Modification" 160 | msgstr "Spara ändringar" 161 | 162 | #: src/window.ui:598 163 | msgid "Save the modification made to this file." 164 | msgstr "Spara ändringar gjorda i denna fil." 165 | 166 | #: src/window.ui:619 167 | msgid "MusicBrainz Tags" 168 | msgstr "MusicBrainz taggar" 169 | 170 | #: src/window.ui:637 171 | msgid "Title " 172 | msgstr "Titel " 173 | 174 | #: src/window.ui:658 175 | msgid "Album " 176 | msgstr "Album " 177 | 178 | #: src/window.ui:678 179 | msgid "Artist " 180 | msgstr "Artist " 181 | 182 | #: src/window.ui:698 183 | msgid "Genre " 184 | msgstr "Genre " 185 | 186 | #: src/window.ui:718 187 | msgid "Track " 188 | msgstr "Spår " 189 | 190 | #: src/window.ui:738 191 | msgid "Year " 192 | msgstr "År " 193 | 194 | #: src/window.ui:753 195 | msgid "Set Tags" 196 | msgstr "Använd taggar" 197 | 198 | #: src/window.ui:757 199 | msgid "Use the MusicBrainz tags" 200 | msgstr "Använd MusicBrainz taggar" 201 | 202 | #: src/window.ui:789 203 | msgid "Lyrics " 204 | msgstr "Texter " 205 | 206 | #: src/window.ui:814 207 | msgid "Set Lyrics" 208 | msgstr "Använd text" 209 | -------------------------------------------------------------------------------- /po/pt_BR.po: -------------------------------------------------------------------------------- 1 | # Portuguese translations for GabTag package. 2 | # Copyright (C) 2019 Ismaïl Lachheb 3 | # This file is distributed under the same license as the gabtag package. 4 | # Automatically generated, 2019. 5 | # Cleiton Floss , 2019. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: gabtag\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-06-06 15:04-0400\n" 12 | "PO-Revision-Date: 2019-06-06 15:15-0400\n" 13 | "Last-Translator: Cleiton Floss \n" 14 | "Language-Team: Portuguese \n" 15 | "Language: pt_BR\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 20 | "X-Generator: Gtranslator 3.32.1\n" 21 | 22 | #: data/com.github.lachhebo.Gabtag.desktop.in:3 23 | msgid "Gabtag" 24 | msgstr "Gabtag" 25 | 26 | #: data/com.github.lachhebo.Gabtag.desktop.in:4 27 | msgid "Modify audio tags" 28 | msgstr "Modifique as etiquetas de áudio" 29 | 30 | #: data/com.github.lachhebo.Gabtag.desktop.in:5 31 | msgid "com.github.lachhebo.Gabtag" 32 | msgstr "com.github.lachhebo.Gabtag" 33 | 34 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:4 35 | msgid "GabTag" 36 | msgstr "GabTag" 37 | 38 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:14 39 | msgid "An audio tagging tool" 40 | msgstr "Uma ferramenta de marcação de áudio" 41 | 42 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:16 43 | msgid "" 44 | "GabTag is a Linux audio tagging tool written in GTK 3, which makes it very " 45 | "suitable for gtk based desktop users." 46 | msgstr "" 47 | "O GabTag é uma ferramenta de marcação de áudio para Linux escrita em GTK 3, " 48 | "o que o torna adequado para usuários de desktop baseados em gtk." 49 | 50 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:20 51 | msgid "" 52 | "It allow users to select several file and modify their tags, It is also " 53 | "possible to directly let GabTag automatically find tags and lyrics for the " 54 | "audio files using online data." 55 | msgstr "" 56 | "Ele permite que os usuários selecionem vários arquivos e modifiquem suas " 57 | "etiquetas. Também é possível permitir que o GabTag encontre automaticamente " 58 | "etiquetas e letras dos áudios usando dados da internet." 59 | 60 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:24 61 | msgid "" 62 | "GabTag is written in Python 3, uses Mutagen to handle audio tag. It also " 63 | "uses MusicBrainz database to find audio tags and lyrics.wikia.com to find " 64 | "lyrics online." 65 | msgstr "" 66 | "GabTag é escrito em Python 3, usa Mutagen para lidar com etiquetas de áudio. " 67 | "Ele também usa o banco de dados MusicBrainz para encontrar etiquetas de " 68 | "áudio e lyrics.wikia.com para encontrar as letras online." 69 | 70 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:38 71 | msgid "Ismaïl Lachheb" 72 | msgstr "Ismaïl Lachheb" 73 | 74 | #: src/window.ui:17 src/window.ui:97 75 | msgid "About" 76 | msgstr "Sobre" 77 | 78 | #: src/window.ui:21 79 | msgid "GabTag is an open-source audio tagging tool written in gtk3." 80 | msgstr "" 81 | "GabTag é uma ferramenta de marcação de áudio de código aberto escrita em " 82 | "gtk3." 83 | 84 | #: src/window.ui:69 85 | msgid "Reset files" 86 | msgstr "Redefinir arquivos" 87 | 88 | #: src/window.ui:83 89 | msgid "Set online tags" 90 | msgstr "Definir etiquetas online" 91 | 92 | #: src/window.ui:134 93 | msgid "Add a Folder to modify tags" 94 | msgstr "Abra uma pasta para modificar etiquetas" 95 | 96 | #: src/window.ui:138 97 | msgid "Open" 98 | msgstr "Abrir" 99 | 100 | #: src/window.ui:167 101 | msgid "Save All" 102 | msgstr "Salvar Tudo" 103 | 104 | #: src/window.ui:211 105 | msgid "Name" 106 | msgstr "Nome" 107 | 108 | #: src/window.ui:252 109 | msgid "Album" 110 | msgstr "Álbum" 111 | 112 | #: src/window.ui:270 113 | msgid "Artist" 114 | msgstr "Artista" 115 | 116 | #: src/window.ui:288 117 | msgid "Genre" 118 | msgstr "Gênero" 119 | 120 | #: src/window.ui:306 121 | msgid "Title" 122 | msgstr "Título" 123 | 124 | #: src/window.ui:394 125 | msgid "Cover" 126 | msgstr "Capa" 127 | 128 | #: src/window.ui:406 129 | msgid "Load" 130 | msgstr "Carregar" 131 | 132 | #: src/window.ui:410 133 | msgid "Load a cover (jpeg or png)" 134 | msgstr "Carregar uma capa (jpeg ou png)" 135 | 136 | #: src/window.ui:453 137 | msgid "Track number" 138 | msgstr "Número da musica" 139 | 140 | #: src/window.ui:471 141 | msgid "Year" 142 | msgstr "Ano" 143 | 144 | #: src/window.ui:523 145 | msgid "Length" 146 | msgstr "Duração" 147 | 148 | #: src/window.ui:538 149 | msgid "Size" 150 | msgstr "Tamanho" 151 | 152 | #: src/window.ui:575 153 | msgid "Reset Modification" 154 | msgstr "Redefinir modificação" 155 | 156 | #: src/window.ui:579 157 | msgid "Remove modification made to fields." 158 | msgstr "Redefinir modificação feitas aos campos." 159 | 160 | #: src/window.ui:594 161 | msgid "Save Modification" 162 | msgstr "Salvar modificações" 163 | 164 | #: src/window.ui:598 165 | msgid "Save the modification made to this file." 166 | msgstr "Salvar as modificações feitas a esse arquivo." 167 | 168 | #: src/window.ui:619 169 | msgid "MusicBrainz Tags" 170 | msgstr "Etiquetas do MusicBrainz" 171 | 172 | #: src/window.ui:637 173 | msgid "Title " 174 | msgstr "Título" 175 | 176 | #: src/window.ui:658 177 | msgid "Album " 178 | msgstr "Álbum" 179 | 180 | #: src/window.ui:678 181 | msgid "Artist " 182 | msgstr "Artista" 183 | 184 | #: src/window.ui:698 185 | msgid "Genre " 186 | msgstr "Gênero" 187 | 188 | #: src/window.ui:718 189 | msgid "Track " 190 | msgstr "Número" 191 | 192 | #: src/window.ui:738 193 | msgid "Year " 194 | msgstr "Ano" 195 | 196 | #: src/window.ui:753 197 | msgid "Set Tags" 198 | msgstr "Definir etiquetas" 199 | 200 | #: src/window.ui:757 201 | msgid "Use the MusicBrainz tags" 202 | msgstr "Usar etiquetas do MusicBrainz" 203 | 204 | #: src/window.ui:789 205 | msgid "Lyrics " 206 | msgstr "Letra " 207 | 208 | #: src/window.ui:814 209 | msgid "Set Lyrics" 210 | msgstr "Definir letra" 211 | -------------------------------------------------------------------------------- /po/fr.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the gabtag package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Ismaïl Lachheb , 2019 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: gabtag\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2019-06-06 15:04-0400\n" 15 | "PO-Revision-Date: 2019-06-11 19:37+0000\n" 16 | "Last-Translator: Ismaïl Lachheb , 2019\n" 17 | "Language-Team: French (France) (https://www.transifex.com/lachhebo/teams/100040/fr_FR/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: fr_FR\n" 22 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 23 | 24 | #: data/com.github.lachhebo.Gabtag.desktop.in:3 25 | msgid "Gabtag" 26 | msgstr "Gabtag" 27 | 28 | #: data/com.github.lachhebo.Gabtag.desktop.in:4 29 | msgid "Modify audio tags" 30 | msgstr "Modifier les tags audios" 31 | 32 | #: data/com.github.lachhebo.Gabtag.desktop.in:5 33 | msgid "com.github.lachhebo.Gabtag" 34 | msgstr "com.github.lachhebo.Gabtag" 35 | 36 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:4 37 | msgid "GabTag" 38 | msgstr "GabTag" 39 | 40 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:14 41 | msgid "An audio tagging tool" 42 | msgstr "Un outils d'édition des tags audios" 43 | 44 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:16 45 | msgid "" 46 | "GabTag is a Linux audio tagging tool written in GTK 3, which makes it very " 47 | "suitable for gtk based desktop users." 48 | msgstr "" 49 | "GabTag est une application Linux d'édition de balise de fichier audios " 50 | "écrite en GTK 3. Elle est donc parfaitement adaptée aux utilisateurs de " 51 | "bureau basés sur GTK." 52 | 53 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:20 54 | msgid "" 55 | "It allow users to select several file and modify their tags, It is also " 56 | "possible to directly let GabTag automatically find tags and lyrics for the " 57 | "audio files using online data." 58 | msgstr "" 59 | "Il est possible de modifier les balises de plusieurs fichier. Il est aussi " 60 | "possible de laisser GabTag automatiquement trouver les balises et les lyrics" 61 | " associés à un fichier en utilisant des données provenant d'internet. " 62 | 63 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:24 64 | msgid "" 65 | "GabTag is written in Python 3, uses Mutagen to handle audio tag. It also " 66 | "uses MusicBrainz database to find audio tags and lyrics.wikia.com to find " 67 | "lyrics online." 68 | msgstr "" 69 | "Gabtag est écrit en Python 3, il utilise Mutagen pour gérer les fichiers " 70 | "audios, il utilise aussi la base de données MusicBrainz pour récupérer les " 71 | "balises et la base de données lyrics.wikia.com pour récupérer les paroles. " 72 | 73 | #: data/com.github.lachhebo.Gabtag.appdata.xml.in:38 74 | msgid "Ismaïl Lachheb" 75 | msgstr "Ismaïl Lachheb" 76 | 77 | #: src/window.ui:17 src/window.ui:97 78 | msgid "About" 79 | msgstr "À propos" 80 | 81 | #: src/window.ui:21 82 | msgid "gabtag is an open-source audio tagging tool written in gtk3." 83 | msgstr "gabtag est un éditeur de balise audio écrit en GTK3. " 84 | 85 | #: src/window.ui:69 86 | msgid "Reset files" 87 | msgstr "Réinitialiser les fichiers" 88 | 89 | #: src/window.ui:83 90 | msgid "Set online tags" 91 | msgstr "Étiqueter avec les balises internet" 92 | 93 | #: src/window.ui:132 94 | msgid "Add a Folder to modify tags" 95 | msgstr "Ajouter un dossier pour modifier les balises" 96 | 97 | #: src/window.ui:134 98 | msgid "Open" 99 | msgstr "Ouvrir" 100 | 101 | #: src/window.ui:167 102 | msgid "Save All" 103 | msgstr "Sauvegarder Tout" 104 | 105 | #: src/window.ui:211 106 | msgid "Name" 107 | msgstr "Nom" 108 | 109 | #: src/window.ui:252 110 | msgid "Album" 111 | msgstr "Album" 112 | 113 | #: src/window.ui:270 114 | msgid "Artist" 115 | msgstr "Artiste" 116 | 117 | #: src/window.ui:288 118 | msgid "Genre" 119 | msgstr "Genre" 120 | 121 | #: src/window.ui:306 122 | msgid "Title" 123 | msgstr "Titre" 124 | 125 | #: src/window.ui:394 126 | msgid "Cover" 127 | msgstr "Illustration" 128 | 129 | #: src/window.ui:406 130 | msgid "Load" 131 | msgstr "Charger" 132 | 133 | #: src/window.ui:410 134 | msgid "Load a cover (jpeg or png)" 135 | msgstr "Charger une illustration (jpeg ou png)" 136 | 137 | #: src/window.ui:453 138 | msgid "Track number" 139 | msgstr "Numéro de titre" 140 | 141 | #: src/window.ui:471 142 | msgid "Year" 143 | msgstr "Année" 144 | 145 | #: src/window.ui:523 146 | msgid "Length" 147 | msgstr "Longueur" 148 | 149 | #: src/window.ui:538 150 | msgid "Size" 151 | msgstr "Taille" 152 | 153 | #: src/window.ui:575 154 | msgid "Reset Modification" 155 | msgstr "Réinitialiser les modifications" 156 | 157 | #: src/window.ui:579 158 | msgid "Remove modification made to fields." 159 | msgstr "Annule les modifications appliquées au fichier" 160 | 161 | #: src/window.ui:593 162 | msgid "Save Modification" 163 | msgstr "Sauvegarder les modifications" 164 | 165 | #: src/window.ui:598 166 | msgid "Save the modification made to this file." 167 | msgstr "Sauvegarde les modifications appliquées au fichier" 168 | 169 | #: src/window.ui:619 170 | msgid "MusicBrainz Tags" 171 | msgstr "Balises MusicBrainz" 172 | 173 | #: src/window.ui:637 174 | msgid "Title " 175 | msgstr "Titre" 176 | 177 | #: src/window.ui:658 178 | msgid "Album " 179 | msgstr "Album" 180 | 181 | #: src/window.ui:678 182 | msgid "Artist " 183 | msgstr "Artiste" 184 | 185 | #: src/window.ui:698 186 | msgid "Genre " 187 | msgstr "Genre" 188 | 189 | #: src/window.ui:718 190 | msgid "Track " 191 | msgstr "Numéro de titre" 192 | 193 | #: src/window.ui:738 194 | msgid "Year " 195 | msgstr "Année" 196 | 197 | #: src/window.ui:753 198 | msgid "Set Tags" 199 | msgstr "Appliquer ces balises" 200 | 201 | #: src/window.ui:757 202 | msgid "Use the MusicBrainz tags" 203 | msgstr "Utiliser les balises MusicBrainz" 204 | 205 | #: src/window.ui:789 206 | msgid "Lyrics " 207 | msgstr "Paroles" 208 | 209 | #: src/window.ui:814 210 | msgid "Set Lyrics" 211 | msgstr "Appliquer ces paroles" 212 | -------------------------------------------------------------------------------- /src/event_machine.py: -------------------------------------------------------------------------------- 1 | from .dir_manager import DIR_MANAGER 2 | from .model import MODEL 3 | from .selection_handler import SELECTION 4 | from .controller import Controller 5 | from .tools import add_filters, get_filenames_from_selection 6 | 7 | from gi.repository import Gio, GLib, Gtk 8 | 9 | import logging 10 | import gi 11 | import gettext 12 | 13 | gi.require_version("Gtk", "4.0") 14 | _ = gettext.gettext 15 | 16 | 17 | class EventMachine: 18 | def __init__(self) -> None: 19 | self.window = None 20 | self.is_real_selection = 0 21 | 22 | def on_but_saved_clicked(self, widget): 23 | if DIR_MANAGER.is_open_directory: 24 | Controller.crawl_thread_modification() 25 | 26 | def on_clicked_save_one(self, widget): 27 | if self.is_real_selection == 1: 28 | self.is_real_selection = 0 29 | Controller.save_some_files() 30 | self.is_real_selection = 1 31 | 32 | def on_reset_one_clicked(self, widget): 33 | if self.is_real_selection == 1: 34 | self.is_real_selection = 0 35 | # Controller.reset_some_files() 36 | Controller.reset_all() 37 | self.is_real_selection = 1 38 | 39 | def on_reset_all_clicked(self, widget, action: Gio.Action): 40 | if self.is_real_selection == 1: 41 | self.is_real_selection = 0 42 | Controller.reset_all() 43 | self.is_real_selection = 1 44 | 45 | def on_about_clicked(self, widget, action: Gio.Action): 46 | if self.window is not None: 47 | self.window.id_about_window.set_visible(True) 48 | 49 | def on_open_clicked(self, widget): 50 | self.is_real_selection = 0 51 | dialog = Gtk.FileDialog.new() 52 | dialog.set_title(_("Select a Folder")) 53 | dialog.set_modal(True) 54 | 55 | dialog.select_folder(self.window, None, self.on_open_folder_chooser) 56 | 57 | self.is_real_selection = 1 58 | 59 | def on_open_folder_chooser(self, dialog, result): 60 | try: 61 | gfile = dialog.select_folder_finish(result) 62 | except GLib.Error as err: 63 | logging.debug("Could not open folder: %s", err.message) 64 | else: 65 | Controller.change_directory(gfile.get_path()) 66 | # print("directory changed !", gfile.get_path()) 67 | 68 | def on_menu_but_toggled(self, widget): 69 | pass 70 | 71 | def on_title_changed(self, widget): 72 | if self.is_real_selection == 1: 73 | self.is_real_selection = 0 74 | Controller.react_to_user_modif("title", widget.get_text()) 75 | self.is_real_selection = 1 76 | 77 | def on_artist_changed(self, widget): 78 | if self.is_real_selection == 1: 79 | self.is_real_selection = 0 80 | Controller.react_to_user_modif("artist", widget.get_text()) 81 | self.is_real_selection = 1 82 | 83 | def on_album_changed(self, widget): 84 | if self.is_real_selection == 1: 85 | self.is_real_selection = 0 86 | Controller.react_to_user_modif("album", widget.get_text()) 87 | self.is_real_selection = 1 88 | 89 | def on_type_changed(self, widget): 90 | if self.is_real_selection == 1: 91 | self.is_real_selection = 0 92 | Controller.react_to_user_modif("genre", widget.get_text()) 93 | self.is_real_selection = 1 94 | 95 | def on_track_changed(self, widget): 96 | if self.is_real_selection == 1: 97 | self.is_real_selection = 0 98 | Controller.react_to_user_modif("track", widget.get_text()) 99 | self.is_real_selection = 1 100 | 101 | def on_year_changed(self, widget): 102 | if self.is_real_selection == 1: 103 | self.is_real_selection = 0 104 | Controller.react_to_user_modif("year", widget.get_text()) 105 | self.is_real_selection = 1 106 | 107 | def on_load_cover_clicked(self, widget): 108 | if self.is_real_selection == 1: 109 | self.is_real_selection = 0 110 | 111 | dialog = Gtk.FileDialog.new() 112 | dialog.set_title(_("Open a File")) 113 | dialog.set_modal(True) 114 | 115 | add_filters(dialog) 116 | 117 | dialog.open(self.window, None, self.on_open_image_chooser) 118 | 119 | self.is_real_selection = 1 120 | 121 | def on_open_image_chooser(self, dialog, result): 122 | try: 123 | gfile = dialog.open_finish(result) 124 | except GLib.Error as err: 125 | logging.debug("Could not open file: %s", err.message) 126 | else: 127 | name_files = get_filenames_from_selection(SELECTION.selection) 128 | MODEL.update_modifications( 129 | name_files, "cover", gfile.get_path(), DIR_MANAGER.directory 130 | ) 131 | Controller.update_view(name_files) 132 | 133 | def on_selected_changed(self, selection): 134 | if self.is_real_selection == 1: 135 | self.is_real_selection = 0 136 | self.update_selection(selection, DIR_MANAGER.directory) 137 | self.is_real_selection = 1 138 | 139 | def update_selection(self, selection, directory): 140 | if SELECTION.has_directory_change is False: 141 | SELECTION.selection = selection 142 | name_files = get_filenames_from_selection(SELECTION.selection) 143 | # print(name_files) 144 | Controller.update_view(name_files) 145 | else: 146 | SELECTION.has_directory_change = False 147 | 148 | def on_set_mbz(self, widget): 149 | if self.is_real_selection == 1: 150 | self.is_real_selection = 0 151 | name_files = get_filenames_from_selection(SELECTION.selection) 152 | MODEL.set_data_crawled(name_files, DIR_MANAGER.directory) 153 | Controller.update_view(name_files) 154 | self.is_real_selection = 1 155 | 156 | def on_set_online_tags(self, widget, action: Gio.Action): 157 | if DIR_MANAGER.is_open_directory and self.is_real_selection == 1: 158 | self.is_real_selection = 0 159 | name_files = get_filenames_from_selection(SELECTION.selection) 160 | MODEL.set_online_tags(DIR_MANAGER.directory) 161 | 162 | if SELECTION.selection is not None: 163 | Controller.update_view(name_files) 164 | 165 | self.is_real_selection = 1 166 | 167 | 168 | EVENT_MACHINE = EventMachine() 169 | -------------------------------------------------------------------------------- /src/view.py: -------------------------------------------------------------------------------- 1 | import io 2 | # import math 3 | import gi 4 | from threading import RLock 5 | from PIL import Image 6 | from gi.repository import GdkPixbuf, GLib 7 | 8 | from .tools import set_text_widget_permission, set_label 9 | 10 | gi.require_version("Gtk", "4.0") 11 | 12 | 13 | verrou_tags = RLock() 14 | verrou_mbz = RLock() 15 | 16 | 17 | class View: 18 | def __init__(self): 19 | """ 20 | Here, we initialise the widget we are going to use in the future. 21 | """ 22 | 23 | self.tree_view = None 24 | self.title = None 25 | self.album = None 26 | self.artist = None 27 | self.genre = None 28 | self.cover = None 29 | self.track = None 30 | self.year = None 31 | self.length = None 32 | self.size = None 33 | 34 | # size of the cover 35 | self.cover_width = 1000 36 | self.cover_height = 1000 37 | self.last_cover = "" 38 | 39 | self.title_mbz = None 40 | self.album_mbz = None 41 | self.artist_mbz = None 42 | self.genre_mbz = None 43 | self.cover_mbz = None 44 | self.track_mbz = None 45 | self.year_mbz = None 46 | 47 | def show_mbz(self, data_scrapped): 48 | with verrou_mbz: 49 | 50 | # We show the tag currently in tag_dico 51 | self.title_mbz.set_text(data_scrapped["title"]) 52 | self.track_mbz.set_text(data_scrapped["track"]) 53 | self.genre_mbz.set_text(data_scrapped["genre"]) 54 | self.album_mbz.set_text(data_scrapped["album"]) 55 | self.artist_mbz.set_text(data_scrapped["artist"]) 56 | self.year_mbz.set_text(data_scrapped["year"]) 57 | 58 | if data_scrapped["cover"] != "": 59 | with Image.open(io.BytesIO(data_scrapped["cover"])) as img: 60 | 61 | try: 62 | glib_bytes = GLib.Bytes.new(img.tobytes()) 63 | pixbuf = GdkPixbuf.Pixbuf.new_from_bytes( 64 | glib_bytes, 65 | GdkPixbuf.Colorspace.RGB, 66 | False, 67 | 8, 68 | img.width, 69 | img.height, 70 | len(img.getbands()) * img.width, 71 | ) 72 | 73 | pixbuf = pixbuf.scale_simple( 74 | 250, 250, GdkPixbuf.InterpType.BILINEAR 75 | ) 76 | 77 | self.cover_mbz.set_from_pixbuf(pixbuf) 78 | except TypeError: 79 | self.cover_mbz.set_from_icon_name("emblem-music-symbolic") 80 | else: 81 | self.cover_mbz.set_from_icon_name("emblem-music-symbolic") 82 | 83 | def erase(self): 84 | """ 85 | We erase value written in the GtkEntry of each of those tags 86 | """ 87 | self.genre.set_text("") 88 | self.album.set_text("") 89 | self.title.set_text("") 90 | self.artist.set_text("") 91 | self.year.set_text("") 92 | self.track.set_text("") 93 | self.cover.set_from_icon_name("emblem-music-symbolic") 94 | self.last_cover = "" 95 | self.show_mbz( 96 | { 97 | "title": "", 98 | "track": "", 99 | "album": "", 100 | "genre": "", 101 | "artist": "", 102 | "cover": "", 103 | "year": "", 104 | } 105 | ) 106 | 107 | def show_cover_from_bytes(self, bytes_file): 108 | with Image.open(io.BytesIO(bytes_file)) as img: 109 | glib_bytes = GLib.Bytes.new(img.tobytes()) 110 | 111 | width = img.width # The best fix i could find for the moment 112 | height = img.height 113 | # if glib_bytes.get_size() < width * height * 3: 114 | # width = math.sqrt(glib_bytes.get_size() / 3) 115 | # height = math.sqrt(glib_bytes.get_size() / 3) 116 | 117 | pixbuf = GdkPixbuf.Pixbuf.new_from_bytes( 118 | glib_bytes, 119 | GdkPixbuf.Colorspace.RGB, 120 | False, 121 | 8, 122 | width, 123 | height, 124 | len(img.getbands()) * img.width, 125 | ) 126 | 127 | pixbuf = pixbuf.scale_simple( 128 | self.cover_width, self.cover_height, GdkPixbuf.InterpType.BILINEAR 129 | ) 130 | 131 | self.cover.set_from_pixbuf(pixbuf) 132 | 133 | def show_cover_from_file(self, name_file): 134 | with Image.open(name_file) as img: 135 | glib_bytes = GLib.Bytes.new(img.tobytes()) 136 | 137 | pixbuf = GdkPixbuf.Pixbuf.new_from_bytes( 138 | glib_bytes, 139 | GdkPixbuf.Colorspace.RGB, 140 | False, 141 | 8, 142 | img.width, 143 | img.height, 144 | len(img.getbands()) * img.width, 145 | ) 146 | 147 | pixbuf = pixbuf.scale_simple( 148 | self.cover_width, self.cover_height, GdkPixbuf.InterpType.BILINEAR 149 | ) 150 | 151 | self.cover.set_from_pixbuf(pixbuf) 152 | 153 | def show_tags(self, tags_dict, multiple_rows): 154 | with verrou_tags: 155 | # We show those tags uniquely if there is only one row selected 156 | set_text_widget_permission(self.title, multiple_rows, tags_dict["title"]) 157 | set_text_widget_permission(self.track, multiple_rows, tags_dict["track"]) 158 | 159 | # TODO show size and length for the concatenation of songs 160 | set_label(self.size, multiple_rows, tags_dict["size"]) 161 | set_label(self.length, multiple_rows, tags_dict["length"]) 162 | 163 | # We show the tag currently in tags_dict 164 | self.genre.set_text(tags_dict["genre"]) 165 | self.album.set_text(tags_dict["album"]) 166 | self.artist.set_text(tags_dict["artist"]) 167 | self.year.set_text(tags_dict["year"]) 168 | 169 | # A test to handle if there is a cover 170 | if tags_dict["cover"] != "": 171 | if tags_dict["cover"] != self.last_cover: 172 | # A test to detect bytes file 173 | if isinstance(tags_dict["cover"], bytes): 174 | self.show_cover_from_bytes(tags_dict["cover"]) 175 | self.last_cover = tags_dict["cover"] 176 | else: 177 | self.show_cover_from_file(tags_dict["cover"]) 178 | self.last_cover = tags_dict["cover"] 179 | else: 180 | pass 181 | else: 182 | 183 | self.cover.set_from_icon_name("emblem-music-symbolic") 184 | self.last_cover = "" 185 | 186 | 187 | VIEW = View() 188 | -------------------------------------------------------------------------------- /src/controller.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from typing import List 3 | 4 | from .crawler_data import DATA_CRAWLER 5 | from .crawler_directory import CrawlerDirectory 6 | from .crawler_modification import CrawlerModification 7 | from .dir_manager import DIR_MANAGER 8 | from .model import MODEL 9 | from .selection_handler import SELECTION 10 | from .treeview import TREE_VIEW 11 | from .tools import get_file_list, is_selection_valid, get_filenames_from_selection 12 | 13 | import gi 14 | 15 | from .view import VIEW 16 | 17 | gi.require_version("Gtk", "4.0") 18 | 19 | 20 | class Controller: 21 | @staticmethod 22 | def wait_for_mbz(names_files): 23 | is_waiting_mbz = True 24 | while is_selection_valid(names_files) and is_waiting_mbz: 25 | data_gat = DATA_CRAWLER.get_tags(names_files) 26 | if data_gat is not None and is_selection_valid(names_files): 27 | VIEW.show_mbz(data_gat) 28 | is_waiting_mbz = False 29 | 30 | @staticmethod 31 | def update_directory(directory: str): 32 | SELECTION.update_dir() 33 | DIR_MANAGER.directory = directory 34 | DIR_MANAGER.file_names = get_file_list(directory) 35 | DIR_MANAGER.is_open_directory = True 36 | TREE_VIEW.update_tree_view_list(DIR_MANAGER.file_names) 37 | MODEL.reset_all() 38 | 39 | @staticmethod 40 | def reset_all(): # TODO: check it 41 | """ 42 | Cancel modification before it being saved 43 | and update the view,it supposes that something 44 | is selection (True) 45 | """ 46 | VIEW.erase() 47 | MODEL.reset_all() 48 | # VIEW.update_view() 49 | # TREE_VIEW.manage_bold_font(names_file, add=False) 50 | 51 | @staticmethod 52 | def reset_one(name_files: List): 53 | """ 54 | Find the selected rows and delete the related dictionary 55 | nested in modifications. Then update view 56 | """ 57 | MODEL.reset(name_files) 58 | TREE_VIEW.manage_bold_font(name_files, add=False) 59 | 60 | @staticmethod 61 | def crawl_thread_modification(): 62 | thread = CrawlerModification(MODEL.modification.copy(), TREE_VIEW.store) 63 | MODEL.save_modifications(TREE_VIEW, directory=DIR_MANAGER.directory) 64 | thread.start() 65 | 66 | @staticmethod 67 | def save_some_files(): 68 | name_files = get_filenames_from_selection(SELECTION.selection) 69 | thread = CrawlerModification(MODEL.modification.copy(), name_files) 70 | # print("save for some file") 71 | MODEL.save_modifications( 72 | TREE_VIEW, name_files=name_files, directory=DIR_MANAGER.directory 73 | ) 74 | thread.start() 75 | VIEW.erase() 76 | MODEL.erase_tag() 77 | 78 | try: 79 | multiple_line_selected = MODEL.set_tags_dictionary( 80 | name_files, DIR_MANAGER.directory 81 | ) # return a int 82 | data_scrapped = DATA_CRAWLER.get_tags(name_files) 83 | # print("model", MODEL.tags_dictionary) 84 | # print("multiples", multiple_line_selected) 85 | VIEW.show_tags(MODEL.tags_dictionary, multiple_line_selected) 86 | except Exception: 87 | # print("issue with bug", e) 88 | VIEW.show_mbz( 89 | { 90 | "title": "", 91 | "artist": "", 92 | "album": "", 93 | "track": "", 94 | "year": "", 95 | "genre": "", 96 | "cover": "", 97 | } 98 | ) 99 | 100 | VIEW.show_tags( 101 | { 102 | "title": "", 103 | "album": "", 104 | "artist": "", 105 | "genre": "", 106 | "cover": "", 107 | "year": "", 108 | "track": "", 109 | "length": "", 110 | "size": "", 111 | }, 112 | 1, 113 | ) 114 | return 115 | 116 | if data_scrapped is None: 117 | VIEW.show_mbz( 118 | { 119 | "title": "", 120 | "artist": "", 121 | "album": "", 122 | "track": "", 123 | "year": "", 124 | "genre": "", 125 | "cover": "", 126 | } 127 | ) 128 | 129 | thread_waiting_mbz = Thread( 130 | target=Controller.wait_for_mbz, 131 | args=(name_files,), 132 | ) 133 | thread_waiting_mbz.start() 134 | else: 135 | VIEW.show_mbz(data_scrapped) 136 | 137 | @staticmethod 138 | def reset_some_files(): 139 | name_files = get_filenames_from_selection(SELECTION.selection) 140 | Controller.reset_one(name_files) 141 | 142 | @staticmethod 143 | def change_directory(directory): 144 | VIEW.erase() 145 | SELECTION.update_dir() 146 | Controller.update_directory(directory) 147 | thread = CrawlerDirectory(directory) 148 | thread.start() 149 | 150 | @staticmethod 151 | def react_to_user_modif(tag: str, text: str): 152 | name_files = get_filenames_from_selection(SELECTION.selection) 153 | MODEL.update_modifications(name_files, tag, text, DIR_MANAGER.directory) 154 | TREE_VIEW.manage_bold_font(name_files) 155 | 156 | @staticmethod 157 | def update_view(names_files: List): 158 | """ 159 | Erase the view and the current tag value then get tags for 160 | selected row (or rows) and show them. 161 | """ 162 | VIEW.erase() 163 | MODEL.erase_tag() 164 | 165 | try: 166 | multiple_line_selected = MODEL.set_tags_dictionary( 167 | names_files, DIR_MANAGER.directory 168 | ) # return a int 169 | data_scrapped = DATA_CRAWLER.get_tags(names_files) 170 | VIEW.show_tags(MODEL.tags_dictionary, multiple_line_selected) 171 | except Exception: 172 | # print("issue with bug", e) 173 | VIEW.show_mbz( 174 | { 175 | "title": "", 176 | "artist": "", 177 | "album": "", 178 | "track": "", 179 | "year": "", 180 | "genre": "", 181 | "cover": "", 182 | } 183 | ) 184 | 185 | VIEW.show_tags( 186 | { 187 | "title": "", 188 | "album": "", 189 | "artist": "", 190 | "genre": "", 191 | "cover": "", 192 | "year": "", 193 | "track": "", 194 | "length": "", 195 | "size": "", 196 | }, 197 | 1, 198 | ) 199 | return 200 | 201 | if data_scrapped is None: 202 | VIEW.show_mbz( 203 | { 204 | "title": "", 205 | "artist": "", 206 | "album": "", 207 | "track": "", 208 | "year": "", 209 | "genre": "", 210 | "cover": "", 211 | } 212 | ) 213 | 214 | thread_waiting_mbz = Thread( 215 | target=Controller.wait_for_mbz, 216 | args=(names_files,), 217 | ) 218 | thread_waiting_mbz.start() 219 | else: 220 | VIEW.show_mbz(data_scrapped) 221 | -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, call, patch 2 | 3 | from src.model import Model 4 | 5 | TESTED_MODULE = "src.model" 6 | 7 | 8 | def test_reset_all__clear_view_modificaton_and_tree(): 9 | # given 10 | model = Model() 11 | model.modification = { 12 | "fake": "", 13 | } 14 | 15 | # when 16 | model.reset_all() 17 | 18 | # then 19 | assert model.modification == {} 20 | 21 | 22 | def test_update_tags_dictionary__update_tags_dictionary_when_a_file_is_modified(): 23 | # Arrange 24 | testmodel = Model() 25 | testmodel.modification = {"testkey": {"album": "a", "artist": "c"}} 26 | 27 | # Act 28 | testmodel.update_tags_dictionary_with_modification("testkey") 29 | 30 | # Assert 31 | assert "a" == testmodel.tags_dictionary["album"] 32 | assert "c" == testmodel.tags_dictionary["artist"] 33 | 34 | 35 | def test_check_tag_equal_key_value_return_1_if_tag_is_equal_to_expected_value(): 36 | # Arrange 37 | testmodel = Model() 38 | testmodel.modification = {"testkey": {"album": "nqnt", "artist": "vald"}} 39 | 40 | # Act 41 | value_test1 = testmodel.check_tag_equal_key_value( 42 | 0, "", "testkey", "artist", "vald" 43 | ) 44 | value_test2 = testmodel.check_tag_equal_key_value( 45 | 1, "xeu", "testkey", "album", "xeu" 46 | ) 47 | 48 | # Assert 49 | assert value_test1 == 1 50 | assert value_test2 == 0 51 | 52 | 53 | def test_erase_tag_results_in_erasing_tags_dictionary(): 54 | # Given 55 | mymodel = Model() 56 | mymodel.tags_dictionary["title"] = "jojo" 57 | 58 | # When 59 | mymodel.erase_tag() 60 | 61 | # Then 62 | for key in mymodel.tags_dictionary: 63 | assert mymodel.tags_dictionary[key] == "" 64 | 65 | 66 | @patch(f"{TESTED_MODULE}.get_file_manager") 67 | def test_save_modifications__set_tags_for_each_file_in_modification(mock_audio): 68 | # Given 69 | testmodel = Model() 70 | testmodel.modification = { 71 | "ost.mp4": { 72 | "title": "naruto", 73 | }, 74 | } 75 | audio = Mock() 76 | mock_tree = Mock() 77 | mock_audio.return_value = audio 78 | 79 | # When 80 | testmodel.save_modifications(mock_tree) 81 | 82 | # Then 83 | audio.set_tag.assert_called_with("title", "naruto") 84 | assert testmodel.modification == {"ost.mp4": {}} 85 | 86 | 87 | @patch(f"{TESTED_MODULE}.get_file_manager") 88 | def test_save_modifications__remove_bold_fonts_for_each_file_in_modification( 89 | mock_audio, 90 | ): 91 | # Given 92 | testmodel = Model() 93 | testmodel.modification = { 94 | "ost.mp4": { 95 | "title": "naruto", 96 | }, 97 | } 98 | audio = Mock() 99 | mock_tree = Mock() 100 | mock_audio.return_value = audio 101 | 102 | # When 103 | testmodel.save_modifications(mock_tree) 104 | 105 | # Then 106 | mock_tree.manage_bold_font.assert_called_with(["ost.mp4"], add=False) 107 | 108 | 109 | @patch(f"{TESTED_MODULE}.DATA_CRAWLER") 110 | def test_set_data_crawled__update_modification_with_data_scrapped(mock_data_crawler): 111 | # given 112 | selection = Mock() 113 | selection.get_selected_rows.return_value = ("fake_model", ["file"]) 114 | testmodel = Model() 115 | testmodel.update_modifications = Mock() 116 | mock_data_crawler.get_tags = Mock() 117 | mock_data_crawler.get_tags.return_value = { 118 | "title": "test", 119 | "album": "", 120 | "year": "2020", 121 | "genre": "pop", 122 | "cover": "cov", 123 | } 124 | 125 | calls = [ 126 | call(selection, "title", "test"), 127 | call(selection, "year", "2020"), 128 | call(selection, "genre", "pop"), 129 | call(selection, "cover", "cov"), 130 | ] 131 | 132 | # when 133 | testmodel.set_data_crawled(selection) 134 | 135 | # then 136 | testmodel.update_modifications.assert_has_calls(calls, any_order=True) 137 | 138 | 139 | @patch(f"{TESTED_MODULE}.DATA_CRAWLER") 140 | def test_set_data_crawled__update_modification_with_data_scrapped_if_multiple_files( 141 | mock_data_crawler, 142 | ): 143 | # given 144 | selection = Mock() 145 | selection.get_selected_rows.return_value = ("fake_model", ["file1", "file2"]) 146 | testmodel = Model() 147 | testmodel.update_modifications = Mock() 148 | mock_data_crawler.get_tags = Mock() 149 | mock_data_crawler.get_tags.return_value = { 150 | "title": "test", 151 | "album": "", 152 | "year": "2020", 153 | "genre": "pop", 154 | "cover": "cov", 155 | } 156 | 157 | calls = [ 158 | call(selection, "title", "test"), 159 | call(selection, "year", "2020"), 160 | call(selection, "genre", "pop"), 161 | call(selection, "cover", "cov"), 162 | ] 163 | 164 | # when 165 | testmodel.set_data_crawled(selection) 166 | 167 | # then 168 | testmodel.update_modifications.assert_has_calls(calls, any_order=True) 169 | 170 | 171 | @patch(f"{TESTED_MODULE}.get_file_manager") 172 | def test_file_modified__return_false_if_no_modification(mock_get_file_manager): 173 | # given 174 | testmodel = Model() 175 | audio_mock = Mock() 176 | mock_get_file_manager.return_value = audio_mock 177 | audio_mock.get_tags.return_value = { 178 | "title": "fake_title", 179 | "album": "fake_album", 180 | "type": "fake type", 181 | } 182 | testmodel.modification = {} 183 | 184 | # when 185 | result = testmodel.is_file_modified("fake_file") 186 | 187 | # then 188 | assert result is False 189 | 190 | 191 | @patch(f"{TESTED_MODULE}.get_file_manager") 192 | def test_file_modified__return_false_if_modif_are_equal_to_file(mock_get_file_manager): 193 | # given 194 | testmodel = Model() 195 | audio_mock = Mock() 196 | mock_get_file_manager.return_value = audio_mock 197 | audio_mock.get_tags.return_value = { 198 | "title": "fake_title", 199 | "album": "fake_album", 200 | "type": "fake type", 201 | } 202 | testmodel.modification = { 203 | "fake_file": { 204 | "title": "fake_title", 205 | "album": "fake_album", 206 | "type": "fake type", 207 | }, 208 | } 209 | 210 | # when 211 | result = testmodel.is_file_modified("fake_file") 212 | 213 | # then 214 | assert result is False 215 | 216 | 217 | @patch(f"{TESTED_MODULE}.get_file_manager") 218 | def test_file_modified__return_true_if_modifications(mock_get_file_manager): 219 | # given 220 | testmodel = Model() 221 | audio_mock = Mock() 222 | mock_get_file_manager.return_value = audio_mock 223 | audio_mock.get_tags.return_value = { 224 | "title": "fake_title", 225 | "album": "fake_album", 226 | "type": "fake type", 227 | } 228 | testmodel.modification = { 229 | "fake_file": { 230 | "title": "fake_modified_title", 231 | "album": "fake_album", 232 | "type": "fake type", 233 | }, 234 | } 235 | 236 | # when 237 | result = testmodel.is_file_modified("fake_file") 238 | 239 | # then 240 | assert result is True 241 | 242 | 243 | def test_update_modifications__if_one_file_modified_not_already_in_modifications_update_modification(): 244 | # given 245 | testmodel = Model() 246 | testmodel.is_file_modified = Mock() 247 | 248 | # when 249 | testmodel.update_modifications(["fake_file.mp3"], "title", "arto") 250 | 251 | # then 252 | assert testmodel.modification == { 253 | "fake_file.mp3": { 254 | "title": "arto", 255 | }, 256 | } 257 | 258 | 259 | def test_update_modifications__if_one_file_modified__in_modifications_update_modification(): 260 | # given 261 | testmodel = Model() 262 | testmodel.is_file_modified = Mock() 263 | testmodel.modification = { 264 | "fake_file.mp3": { 265 | "title": "test", 266 | }, 267 | } 268 | 269 | # when 270 | testmodel.update_modifications(["fake_file.mp3"], "title", "arto") 271 | 272 | # then 273 | assert testmodel.modification == { 274 | "fake_file.mp3": { 275 | "title": "arto", 276 | }, 277 | } 278 | -------------------------------------------------------------------------------- /src/model.py: -------------------------------------------------------------------------------- 1 | from typing import List, Any 2 | 3 | from .audio_getter import get_file_manager 4 | from .crawler_data import DATA_CRAWLER 5 | 6 | 7 | class Model: 8 | def __init__(self): 9 | """ 10 | Initialisation of the model class 11 | """ 12 | self.modification = {} 13 | self.tags_dictionary = { 14 | "title": "", 15 | "album": "", 16 | "artist": "", 17 | "genre": "", 18 | "cover": "", 19 | "year": "", 20 | "track": "", 21 | "length": "", 22 | "size": "", 23 | } 24 | self.audio_data = {} 25 | 26 | def reset_all(self) -> None: 27 | self.modification = {} 28 | self.audio_data = {} 29 | 30 | def erase_tag(self) -> None: 31 | for key in self.tags_dictionary: 32 | self.tags_dictionary[key] = "" 33 | 34 | def reset(self, file_names: List) -> None: 35 | for file_name in file_names: 36 | self.modification[file_name] = {} 37 | 38 | def get_modification(self, name_file: str) -> Any: 39 | """ 40 | Find the selected rows, set tags for each row and then save 41 | modification. 42 | We don't need to update the view after saving one file. 43 | """ 44 | if name_file in self.modification: 45 | return self.modification[name_file] 46 | 47 | def update_modification( 48 | self, tag_changed: str, new_value: str, name_file: str, directory: str 49 | ) -> bool: 50 | if name_file not in self.modification: 51 | self.modification[name_file] = {} 52 | 53 | self.modification[name_file][tag_changed] = new_value 54 | 55 | return self.is_file_modified(name_file, directory) 56 | 57 | def update_modifications( 58 | self, file_names: List, tag_changed: str, new_value: str, directory: str 59 | ) -> None: 60 | """ 61 | If the file name is already a key in the directory, add or update 62 | the modified tags else create a new key in modification 63 | """ 64 | 65 | for name_file in file_names: 66 | self.update_modification(tag_changed, new_value, name_file, directory) 67 | 68 | def is_file_modified(self, name_file: str, directory: str) -> bool: 69 | if name_file not in self.modification: 70 | return False 71 | 72 | # audio = get_file_manager(name_file, directory) 73 | if name_file in self.audio_data: 74 | audio = self.audio_data[name_file] 75 | else: 76 | audio = get_file_manager(name_file, directory) 77 | self.audio_data[name_file] = audio 78 | 79 | audio_tag = audio.get_tags() 80 | tag_modified = self.modification[name_file] 81 | 82 | for key_tag in tag_modified: 83 | if tag_modified[key_tag] != audio_tag[key_tag]: 84 | return True 85 | 86 | return False 87 | 88 | def set_online_tags(self, directory) -> None: 89 | for name_file in DATA_CRAWLER.tag_founds: 90 | if name_file not in self.modification: 91 | self.modification[name_file] = {} 92 | 93 | for key in DATA_CRAWLER.tag_founds[name_file]: 94 | self.update_modification( 95 | key, DATA_CRAWLER.tag_founds[name_file][key], name_file, directory 96 | ) 97 | 98 | def save_modifications( 99 | self, tree_handler, directory: str, name_files: List[Any] 100 | ) -> None: 101 | """ 102 | For each key file in modification, we get the tags inside 103 | the nested dictionary and integer them on the audio tag file. 104 | Eventually we save the audio tag file. 105 | """ 106 | 107 | if name_files is None: 108 | modifications = self.modification 109 | else: 110 | modifications = {} 111 | for name in name_files: 112 | modifications[name] = self.modification.get(name) 113 | 114 | for filename in modifications: 115 | if modifications[filename] is not None: 116 | if filename in self.audio_data: 117 | audio = self.audio_data[filename] 118 | else: 119 | audio = get_file_manager(filename, directory) 120 | self.audio_data[filename] = audio 121 | 122 | # audio = get_file_manager(filename, directory=directory) 123 | file_modifications = modifications[filename] 124 | 125 | for key in self.tags_dictionary: 126 | if key in file_modifications: 127 | audio.set_tag(key, file_modifications[key]) 128 | self.tags_dictionary[key] = file_modifications[key] 129 | 130 | tree_handler.manage_bold_font([filename], add=False) 131 | 132 | audio.save_modifications() 133 | self.modification[filename] = {} 134 | 135 | self.update_tags_dictionary_with_modification(filename) 136 | 137 | # print("tags: ", self.tags_dictionary) 138 | # print("modif: " , self.modification) 139 | 140 | def set_tags_dictionary(self, names_file: List, directory: str) -> int: 141 | """ 142 | First we get the selected rows, we get the tag value of the 143 | first row, if there are several rows, we check the tag value 144 | inside them are the same as in the first row. If yes, those tags 145 | value are shown. 146 | """ 147 | 148 | name_file = names_file[0] 149 | 150 | if name_file in self.audio_data: 151 | audio = self.audio_data[name_file] 152 | else: 153 | audio = get_file_manager(name_file, directory) 154 | self.audio_data[name_file] = audio 155 | 156 | for key in self.tags_dictionary: 157 | self.tags_dictionary[key] = audio.get_tag(key) 158 | 159 | if name_file in self.modification: 160 | self.update_tags_dictionary_with_modification(name_file) 161 | 162 | if len(names_file) > 1: 163 | 164 | same_tags = {key: True for key in self.tags_dictionary.keys()} 165 | 166 | for name_file in names_file: 167 | if name_file in self.audio_data: 168 | audio = self.audio_data[name_file] 169 | else: 170 | audio = get_file_manager(name_file, directory) 171 | self.audio_data[name_file] = audio 172 | for key in same_tags: 173 | if same_tags[key] is True: 174 | if key not in ["length", "size"]: 175 | value = self.check_tag_equal_key_value( 176 | audio.check_tag_existence(key), 177 | audio.get_tag(key), 178 | name_file, 179 | key, 180 | self.tags_dictionary[key], 181 | ) 182 | same_tags[key] = value 183 | else: 184 | same_tags[key] = False 185 | for key in same_tags: 186 | if same_tags[key] is False: 187 | self.tags_dictionary[key] = "" 188 | 189 | return 1 190 | else: 191 | return 0 192 | 193 | def update_tags_dictionary_with_modification(self, name_file: str) -> None: 194 | """ 195 | Check in the filename modification dictionary the existence of 196 | tags and update the current list of tags with found values. 197 | """ 198 | 199 | dict_tag_changed = {} 200 | if name_file in self.modification: 201 | dict_tag_changed = self.modification[name_file] 202 | 203 | for key in self.tags_dictionary: 204 | if key in dict_tag_changed: 205 | self.tags_dictionary[key] = dict_tag_changed[key] 206 | 207 | def check_tag_equal_key_value( 208 | self, 209 | audio_key_exist: bool, 210 | audio_tag_value: Any, 211 | name_file: str, 212 | key: str, 213 | key_value: Any, 214 | ) -> bool: 215 | """ 216 | We check that the tag 'key' is equal to key_value 217 | for name_file and return 1 else we return 0.We first 218 | look in modification then in the 219 | tag audio file. 220 | """ 221 | 222 | if name_file in self.modification: 223 | modified_tags = self.modification[name_file] 224 | if key in modified_tags: 225 | if key_value != modified_tags[key]: 226 | return False 227 | else: 228 | return True 229 | 230 | if not audio_key_exist or key_value != audio_tag_value: 231 | return False 232 | 233 | return True 234 | 235 | def set_data_crawled(self, names_files: List, directory: str): 236 | data_scrapped = DATA_CRAWLER.get_tags(names_files) 237 | new_data = {} 238 | 239 | for key in data_scrapped: 240 | if data_scrapped[key] != "": 241 | new_data[key] = data_scrapped[key] 242 | 243 | for key in new_data: 244 | self.update_modifications(names_files, key, new_data[key], directory) 245 | 246 | 247 | MODEL = Model() 248 | -------------------------------------------------------------------------------- /tests/test_event_machine.py: -------------------------------------------------------------------------------- 1 | from src.event_machine import EventMachine 2 | from unittest.mock import Mock, patch 3 | 4 | 5 | from gi.repository import Gtk 6 | 7 | import gi 8 | 9 | 10 | gi.require_version("Gtk", "4.0") 11 | 12 | 13 | TESTED_MODULE = "src.event_machine" 14 | 15 | # title 16 | 17 | 18 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 19 | def test_on_title_change_run_update_modifications(mock_modif): 20 | # given 21 | event_machine = EventMachine() 22 | widget = Mock() 23 | widget.get_text = Mock() 24 | widget.get_text.return_value = "fake_text" 25 | event_machine.is_real_selection = 1 26 | 27 | # when 28 | event_machine.on_title_changed(widget) 29 | 30 | # then 31 | assert event_machine.is_real_selection == 1 32 | mock_modif.assert_called_with("title", "fake_text") 33 | 34 | 35 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 36 | def test_on_title_change__doesnt_run_update_modifications_if_no_selection(mock_modif): 37 | # given 38 | event_machine = EventMachine() 39 | widget = Mock() 40 | widget.get_text = Mock() 41 | widget.get_text.return_value = "fake_text" 42 | event_machine.is_real_selection = 0 43 | 44 | # when 45 | event_machine.on_title_changed(widget) 46 | 47 | # then 48 | mock_modif.assert_not_called() 49 | 50 | 51 | # artist 52 | 53 | 54 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 55 | def test_on_artist_change_run_update_modifications(mock_modif): 56 | # given 57 | event_machine = EventMachine() 58 | widget = Mock() 59 | widget.get_text = Mock() 60 | widget.get_text.return_value = "fake_text" 61 | event_machine.is_real_selection = 1 62 | 63 | # when 64 | event_machine.on_artist_changed(widget) 65 | 66 | # then 67 | assert event_machine.is_real_selection == 1 68 | mock_modif.assert_called_with("artist", "fake_text") 69 | 70 | 71 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 72 | def test_on_artist_change__doesnt_run_update_modifications_if_no_selection(mock_modif): 73 | # given 74 | event_machine = EventMachine() 75 | widget = Mock() 76 | widget.get_text = Mock() 77 | widget.get_text.return_value = "fake_text" 78 | event_machine.is_real_selection = 0 79 | 80 | # when 81 | event_machine.on_artist_changed(widget) 82 | 83 | # then 84 | mock_modif.assert_not_called() 85 | 86 | 87 | # album 88 | 89 | 90 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 91 | def test_on_album_change_run_update_modifications(mock_modif): 92 | # given 93 | event_machine = EventMachine() 94 | widget = Mock() 95 | widget.get_text = Mock() 96 | widget.get_text.return_value = "fake_text" 97 | event_machine.is_real_selection = 1 98 | 99 | # when 100 | event_machine.on_album_changed(widget) 101 | 102 | # then 103 | assert event_machine.is_real_selection == 1 104 | mock_modif.assert_called_with("album", "fake_text") 105 | 106 | 107 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 108 | def test_on_album_change__doesnt_run_update_modifications_if_no_selection(mock_modif): 109 | # given 110 | event_machine = EventMachine() 111 | widget = Mock() 112 | widget.get_text = Mock() 113 | widget.get_text.return_value = "fake_text" 114 | event_machine.is_real_selection = 0 115 | 116 | # when 117 | event_machine.on_album_changed(widget) 118 | 119 | # then 120 | mock_modif.assert_not_called() 121 | 122 | 123 | # genre 124 | 125 | 126 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 127 | def test_on_genre_change_run_update_modifications(mock_modif): 128 | # given 129 | event_machine = EventMachine() 130 | widget = Mock() 131 | widget.get_text = Mock() 132 | widget.get_text.return_value = "fake_text" 133 | event_machine.is_real_selection = 1 134 | 135 | # when 136 | event_machine.on_type_changed(widget) 137 | 138 | # then 139 | assert event_machine.is_real_selection == 1 140 | mock_modif.assert_called_with("genre", "fake_text") 141 | 142 | 143 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 144 | def test_on_genre_change__doesnt_run_update_modifications_if_no_selection(mock_modif): 145 | # given 146 | event_machine = EventMachine() 147 | widget = Mock() 148 | widget.get_text = Mock() 149 | widget.get_text.return_value = "fake_text" 150 | event_machine.is_real_selection = 0 151 | 152 | # when 153 | event_machine.on_type_changed(widget) 154 | 155 | # then 156 | mock_modif.assert_not_called() 157 | 158 | 159 | # track 160 | 161 | 162 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 163 | def test_on_track_change_run_update_modifications(mock_modif): 164 | # given 165 | event_machine = EventMachine() 166 | widget = Mock() 167 | widget.get_text = Mock() 168 | widget.get_text.return_value = "fake_text" 169 | event_machine.is_real_selection = 1 170 | 171 | # when 172 | event_machine.on_track_changed(widget) 173 | 174 | # then 175 | assert event_machine.is_real_selection == 1 176 | mock_modif.assert_called_with("track", "fake_text") 177 | 178 | 179 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 180 | def test_on_track_change__doesnt_run_update_modifications_if_no_selection(mock_modif): 181 | # given 182 | event_machine = EventMachine() 183 | widget = Mock() 184 | widget.get_text = Mock() 185 | widget.get_text.return_value = "fake_text" 186 | event_machine.is_real_selection = 0 187 | 188 | # when 189 | event_machine.on_track_changed(widget) 190 | 191 | # then 192 | mock_modif.assert_not_called() 193 | 194 | 195 | # year 196 | 197 | 198 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 199 | def test_on_year_change_run_update_modifications(mock_modif): 200 | # given 201 | event_machine = EventMachine() 202 | widget = Mock() 203 | widget.get_text = Mock() 204 | widget.get_text.return_value = "fake_text" 205 | event_machine.is_real_selection = 1 206 | 207 | # when 208 | event_machine.on_year_changed(widget) 209 | 210 | # then 211 | assert event_machine.is_real_selection == 1 212 | mock_modif.assert_called_with("year", "fake_text") 213 | 214 | 215 | @patch(f"{TESTED_MODULE}.Controller.react_to_user_modif") 216 | def test_on_year_change__doesnt_run_update_modifications_if_no_selection(mock_modif): 217 | # given 218 | event_machine = EventMachine() 219 | widget = Mock() 220 | widget.get_text = Mock() 221 | widget.get_text.return_value = "fake_text" 222 | event_machine.is_real_selection = 0 223 | 224 | # when 225 | event_machine.on_year_changed(widget) 226 | 227 | # then 228 | mock_modif.assert_not_called() 229 | 230 | 231 | @patch(f"{TESTED_MODULE}.Controller") 232 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 233 | @patch(f"{TESTED_MODULE}.add_filters") 234 | @patch(f"{TESTED_MODULE}.MODEL") 235 | @patch(f"{TESTED_MODULE}.Gtk.FileChooserDialog") 236 | def test_on_load_cover_clicked__run_update_modifcations_and_update_view_if_dialog_return_ok( 237 | mock_d, mock_model, m_filters, m_filenames, m_controller 238 | ): 239 | # given 240 | mock_dialog = Mock() 241 | mock_d.return_value = mock_dialog 242 | mock_dialog.run.return_value = Gtk.ResponseType.OK 243 | event_machine = EventMachine() 244 | event_machine.is_real_selection = 1 245 | widget = Mock() 246 | 247 | # when 248 | event_machine.on_load_cover_clicked(widget) 249 | 250 | # then 251 | 252 | mock_model.update_modifications.assert_called() 253 | m_controller.update_view.assert_called() 254 | 255 | 256 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 257 | @patch(f"{TESTED_MODULE}.Controller") 258 | def test_selected_changed_update_the_view(mock_model, moc_filenames): 259 | # given 260 | event_machine = EventMachine() 261 | event_machine.is_real_selection = 1 262 | selection = Mock() 263 | 264 | # when 265 | event_machine.on_selected_changed(selection) 266 | 267 | # then 268 | mock_model.update_view.assert_called() 269 | 270 | 271 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 272 | @patch(f"{TESTED_MODULE}.Controller") 273 | def test_selected_changed_doesnt_update_the_view_if_no_selection( 274 | mock_model, moc_filenames 275 | ): 276 | # given 277 | event_machine = EventMachine() 278 | event_machine.is_real_selection = 0 279 | selection = Mock() 280 | 281 | # when 282 | event_machine.on_selected_changed(selection) 283 | 284 | # then 285 | mock_model.update_view.assert_not_called() 286 | 287 | 288 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 289 | @patch(f"{TESTED_MODULE}.Controller") 290 | @patch(f"{TESTED_MODULE}.MODEL") 291 | def test_on_set_mbz__update_the_view_if_selection_changed( 292 | m_model, m_controller, m_filenames 293 | ): 294 | # given 295 | event_machine = EventMachine() 296 | event_machine.is_real_selection = 1 297 | widget = Mock() 298 | 299 | # when 300 | event_machine.on_set_mbz(widget) 301 | 302 | # then 303 | m_controller.update_view.assert_called() 304 | m_model.set_data_crawled.assert_called() 305 | 306 | 307 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 308 | @patch(f"{TESTED_MODULE}.Controller") 309 | @patch(f"{TESTED_MODULE}.MODEL") 310 | def test_on_set_mbz__update_the_view_if_no_selection( 311 | m_model, m_controller, m_filenames 312 | ): 313 | # given 314 | event_machine = EventMachine() 315 | event_machine.is_real_selection = 0 316 | widget = Mock() 317 | 318 | # when 319 | event_machine.on_set_mbz(widget) 320 | 321 | # then 322 | m_controller.update_view.assert_not_called() 323 | m_model.set_data_crawled.assert_not_called() 324 | 325 | 326 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 327 | @patch(f"{TESTED_MODULE}.Controller") 328 | @patch(f"{TESTED_MODULE}.SELECTION") 329 | @patch(f"{TESTED_MODULE}.MODEL") 330 | @patch(f"{TESTED_MODULE}.DIR_MANAGER") 331 | def test_on_set_online_tags__set_the_online_tags_using_model_data_and_update_view( 332 | m_dir_manager, m_model, m_selection, m_controller, _ 333 | ): 334 | # given 335 | event_machine = EventMachine() 336 | event_machine.is_real_selection = 1 337 | 338 | m_dir_manager.is_opened_directory = True 339 | widget = Mock() 340 | 341 | # when 342 | event_machine.on_set_online_tags(widget) 343 | 344 | # then 345 | m_model.set_online_tags.assert_called() 346 | m_controller.update_view.assert_called() 347 | 348 | 349 | @patch(f"{TESTED_MODULE}.get_filenames_from_selection") 350 | @patch(f"{TESTED_MODULE}.Controller") 351 | @patch(f"{TESTED_MODULE}.SELECTION") 352 | @patch(f"{TESTED_MODULE}.MODEL") 353 | @patch(f"{TESTED_MODULE}.DIR_MANAGER") 354 | def test_on_set_online_tags__does_notset_the_online_tags_using_model_data_and_update_view_if_no_selection( 355 | m_dir_manager, m_model, m_selection, m_controller, _ 356 | ): 357 | # given 358 | event_machine = EventMachine() 359 | event_machine.is_real_selection = 0 360 | 361 | m_dir_manager.is_opened_directory = True 362 | widget = Mock() 363 | 364 | # when 365 | event_machine.on_set_online_tags(widget) 366 | 367 | # then 368 | m_model.set_online_tags.assert_not_called() 369 | m_controller.update_view.assert_not_called() 370 | 371 | 372 | @patch(f"{TESTED_MODULE}.Controller") 373 | @patch(f"{TESTED_MODULE}.DIR_MANAGER") 374 | def test_on_but_saved_clicked__start_modification_thread(m_dir_manager, m_controller): 375 | # given 376 | event_machine = EventMachine() 377 | m_dir_manager.is_opened_directory = True 378 | widget = Mock() 379 | 380 | # when 381 | event_machine.on_but_saved_clicked(widget) 382 | 383 | # then 384 | m_controller.crawl_thread_modification.assert_called() 385 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/com.github.lachhebo.Gabtag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | Reset Files 7 | win.reset-all 8 | 9 | 10 | Set Online Tags 11 | win.set-online-tags 12 | 13 |
14 |
15 | 16 | About GabTag 17 | win.about 18 | 19 |
20 |
21 | 22 | True 23 | True 24 | GabTag 25 | gpl-3-0 26 | Audio tagging tool. 27 | https://github.com/lachhebo/gabtag 28 | https://github.com/lachhebo/gabtag/issues 29 | Ismaïl Lachheb 30 | Ismaïl Lachheb 31 | Óscar Fernández Díaz 32 | Ismaïl Lachheb 33 | Tobias Bernard 34 | 35 | translator-credits 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 330 |
331 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/com.github.lachhebo.Gabtag.Devel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | --------------------------------------------------------------------------------