├── data ├── Application.css ├── com.github.artemanufrij.playmymusic.gresource.xml ├── icons │ ├── 16 │ │ └── com.github.artemanufrij.playmymusic.svg │ ├── meson.build │ └── symbolic │ │ └── playlist-symbolic.svg ├── meson.build └── com.github.artemanufrij.playmymusic.desktop.in ├── debian ├── compat ├── source │ └── format ├── control ├── rules ├── copyright └── changelog ├── .gitignore ├── screenshots ├── Screenshot.png ├── Screenshot_Artists.png └── Screenshot_Tracks.png ├── po ├── extra │ ├── POTFILES │ ├── meson.build │ └── LINGUAS ├── meson.build ├── POTFILES └── LINGUAS ├── schemas ├── meson.build └── com.github.artemanufrij.playmymusic.gschema.xml ├── .travis.yml ├── meson └── post_install.py ├── meson.build ├── src ├── Widgets │ ├── Views │ │ ├── PlaylistView.vala │ │ ├── Queue.vala │ │ ├── MobilePhone.vala │ │ ├── AlbumView.vala │ │ ├── ArtistsView.vala │ │ └── ArtistView.vala │ ├── TrackTimeLine.vala │ └── Radio.vala ├── meson.build ├── Services │ ├── DeviceManager.vala │ ├── LocalFilesManager.vala │ └── TagManager.vala ├── Interfaces │ ├── MediaKeys.vala │ ├── Inhibitor.vala │ └── SoundIndicator.vala ├── Settings.vala ├── Objects │ ├── Playlist.vala │ ├── Track.vala │ ├── MobilePhoneMusicFolder.vala │ ├── Radio.vala │ ├── AudioCD.vala │ └── TracksContainer.vala ├── Utils │ ├── MusicBrainz.vala │ └── Utils.vala ├── Dialogs │ ├── ArtistEditor.vala │ ├── Preferences.vala │ └── AlbumEditor.vala └── Application.vala └── README.md /data/Application.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *~ 3 | build/ 4 | builddir/ 5 | -------------------------------------------------------------------------------- /data/com.github.artemanufrij.playmymusic.gresource.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshots/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemanufrij/playmymusic/HEAD/screenshots/Screenshot.png -------------------------------------------------------------------------------- /po/extra/POTFILES: -------------------------------------------------------------------------------- 1 | data/com.github.artemanufrij.playmymusic.desktop.in 2 | data/com.github.artemanufrij.playmymusic.appdata.xml.in -------------------------------------------------------------------------------- /screenshots/Screenshot_Artists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemanufrij/playmymusic/HEAD/screenshots/Screenshot_Artists.png -------------------------------------------------------------------------------- /screenshots/Screenshot_Tracks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemanufrij/playmymusic/HEAD/screenshots/Screenshot_Tracks.png -------------------------------------------------------------------------------- /po/extra/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('extra', 2 | args: [ 3 | '--directory=' + meson.source_root(), 4 | '--from-code=UTF-8' 5 | ], 6 | install: false 7 | ) -------------------------------------------------------------------------------- /schemas/meson.build: -------------------------------------------------------------------------------- 1 | install_data( 2 | meson.project_name() + '.gschema.xml', 3 | install_dir: join_paths(get_option('datadir'), 'glib-2.0', 'schemas') 4 | ) 5 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext(meson.project_name(), 2 | args: [ 3 | '--directory=' + meson.source_root(), 4 | '--from-code=UTF-8' 5 | ] 6 | ) 7 | 8 | subdir('extra') -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - lts/* 7 | 8 | sudo: required 9 | 10 | services: 11 | - docker 12 | 13 | addons: 14 | apt: 15 | sources: 16 | - ubuntu-toolchain-r-test 17 | packages: 18 | - libstdc++-5-dev 19 | 20 | install: 21 | - npm i -g @elementaryos/houston 22 | 23 | script: 24 | - houston ci -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | icon_sizes = ['16', '24', '32', '48', '64'] 2 | 3 | foreach i : icon_sizes 4 | install_data( 5 | join_paths(i, meson.project_name() + '.svg'), 6 | install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', i + 'x' + i, 'apps') 7 | ) 8 | endforeach 9 | 10 | install_data( 11 | join_paths('symbolic', 'playlist-queue-symbolic.svg'), 12 | install_dir: join_paths(get_option('datadir'), 'icons/hicolor/symbolic/apps') 13 | ) 14 | 15 | install_data( 16 | join_paths('symbolic', 'playlist-symbolic.svg'), 17 | install_dir: join_paths(get_option('datadir'), 'icons/hicolor/symbolic/apps') 18 | ) 19 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: com.github.artemanufrij.playmymusic 2 | Section: x11 3 | Priority: optional 4 | Maintainer: Artem Anufrij 5 | Build-Depends: appstream, 6 | debhelper (>= 9), 7 | gettext, 8 | desktop-file-utils, 9 | libgranite-dev, 10 | libjson-glib-dev, 11 | libsoup2.4-dev, 12 | libsqlite3-dev, 13 | libgstreamer-plugins-base1.0-dev, 14 | libtagc0-dev, 15 | meson, 16 | valac (>= 0.26) 17 | Standards-Version: 4.1.1 18 | 19 | Package: com.github.artemanufrij.playmymusic 20 | Architecture: any 21 | Depends: ${misc:Depends}, ${shlibs:Depends} 22 | Description: Simple music player 23 | A music player for listening local music files and online radios, written especially for elementary OS 24 | -------------------------------------------------------------------------------- /meson/post_install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | 6 | prefix = os.environ.get('MESON_INSTALL_PREFIX', '/usr') 7 | datadir = os.path.join(prefix, 'share') 8 | 9 | # Packaging tools define DESTDIR and this isn't needed for them 10 | if 'DESTDIR' not in os.environ: 11 | 12 | 13 | print('Compiling gsettings schemas…') 14 | schema_dir = os.path.join(datadir, 'glib-2.0/schemas') 15 | subprocess.call(['glib-compile-schemas', schema_dir]) 16 | 17 | print('Updating icon cache…') 18 | icon_cache_dir = os.path.join(datadir, 'icons', 'hicolor') 19 | 20 | subprocess.call(['gtk-update-icon-cache', '-qtf', icon_cache_dir]) 21 | 22 | print('Updating desktop database…') 23 | desktop_database_dir = os.path.join(datadir, 'applications') 24 | subprocess.call(['update-desktop-database', '-q', desktop_database_dir]) 25 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ 14 | 15 | override_dh_auto_clean: 16 | rm -rf debian/build 17 | 18 | override_dh_auto_configure: 19 | mkdir -p debian/build 20 | cd debian/build && meson --prefix=/usr ../.. 21 | 22 | override_dh_auto_build: 23 | cd debian/build && ninja -v 24 | 25 | override_dh_auto_test: 26 | cd debian/build && ninja test 27 | 28 | override_dh_auto_install: 29 | cd debian/build && DESTDIR=${CURDIR}/debian/com.github.artemanufrij.playmymusic ninja install -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://dep.debian.net/deps/dep5 2 | Upstream-Name: com.github.artemanufrij.regextester 3 | Source: https://github.com/artemanufrij/regextester 4 | 5 | Files: * 6 | Copyright: Artem Anufrij 7 | License: GPL-3.0+ 8 | 9 | License: GPL-3.0+ 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | . 15 | This package is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | . 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | . 23 | On Debian systems, the complete text of the GNU General 24 | Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". 25 | 26 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | 2 | i18n.merge_file( 3 | input: meson.project_name() + '.desktop.in', 4 | output: meson.project_name() + '.desktop', 5 | po_dir: join_paths(meson.source_root(), 'po', 'extra'), 6 | type: 'desktop', 7 | install: true, 8 | install_dir: join_paths(get_option('datadir'), 'applications') 9 | ) 10 | 11 | i18n.merge_file( 12 | input: meson.project_name() + '.appdata.xml.in', 13 | output: meson.project_name() + '.appdata.xml', 14 | po_dir: join_paths(meson.source_root(), 'po', 'extra'), 15 | install: true, 16 | install_dir: join_paths(get_option('datadir'), 'metainfo') 17 | ) 18 | 19 | desktop_file_validate = find_program('desktop-file-validate', required:false) 20 | 21 | if desktop_file_validate.found() 22 | test ( 23 | 'Validate desktop file', 24 | desktop_file_validate, 25 | args: join_paths(meson.current_build_dir (), meson.project_name() + '.desktop') 26 | ) 27 | endif 28 | 29 | appstreamcli = find_program(['appstreamcli', 'appstream-util'], required:false) 30 | 31 | if appstreamcli.found() 32 | test ( 33 | 'Validate appdata file', 34 | appstreamcli, 35 | args: ['validate', join_paths(meson.current_build_dir (), meson.project_name() + '.appdata.xml')] 36 | ) 37 | endif 38 | 39 | 40 | subdir('icons') -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('com.github.artemanufrij.playmymusic', ['vala', 'c'], version: '0.6.9') 2 | 3 | 4 | gnome = import('gnome') 5 | i18n = import('i18n') 6 | 7 | # Add vapi files 8 | add_project_arguments( 9 | [ 10 | '--vapidir', 11 | join_paths(meson.current_source_dir(), 'vapi') 12 | ], 13 | language: 'vala' 14 | ) 15 | 16 | 17 | # Add locale support 18 | conf = configuration_data() 19 | conf.set_quoted('GETTEXT_PACKAGE', meson.project_name()) 20 | conf.set_quoted('PREFIX', get_option('prefix')) 21 | conf.set_quoted('VERSION', meson.project_version()) 22 | conf.set_quoted('PACKAGE', meson.project_name()) 23 | conf.set_quoted('LOCALE_DIR', join_paths(get_option('prefix'), get_option('localedir'))) 24 | conf.set_quoted('DATADIR', join_paths(get_option('prefix'), get_option('datadir'))) 25 | conf.set_quoted('PKGDATADIR', join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())) 26 | 27 | configure_file( 28 | output: 'config.h', 29 | configuration: conf 30 | ) 31 | config_h_dir = include_directories('src') 32 | 33 | # Arguments C - no gcc warnings 34 | c_args = [ 35 | '-include', 'config.h', 36 | '-w', '-DGETTEXT_PACKAGE="' + meson.project_name() + '"' 37 | ] 38 | 39 | 40 | subdir('data') 41 | subdir('po') 42 | subdir('schemas') 43 | subdir('src') 44 | meson.add_install_script('meson/post_install.py') 45 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | src/Application.vala 2 | src/MainWindow.vala 3 | src/Settings.vala 4 | src/Dialogs/AlbumEditor.vala 5 | src/Dialogs/ArtistEditor.vala 6 | src/Dialogs/Preferences.vala 7 | src/Interfaces/Inhibitor.vala 8 | src/Interfaces/MediaKeys.vala 9 | src/Interfaces/SoundIndicator.vala 10 | src/Objects/Album.vala 11 | src/Objects/Artist.vala 12 | src/Objects/AudioCD.vala 13 | src/Objects/MobilePhone.vala 14 | src/Objects/MobilePhoneMusicFolder.vala 15 | src/Objects/Playlist.vala 16 | src/Objects/Radio.vala 17 | src/Objects/Track.vala 18 | src/Objects/TracksContainer.vala 19 | src/Services/DataBaseManager.vala 20 | src/Services/DeviceManager.vala 21 | src/Services/LibraryManager.vala 22 | src/Services/LocalFilesManager.vala 23 | src/Services/MusicBrainzManager.vala 24 | src/Services/Player.vala 25 | src/Services/TagManager.vala 26 | src/Utils/MusicBrainz.vala 27 | src/Utils/Utils.vala 28 | src/Widgets/Album.vala 29 | src/Widgets/Artist.vala 30 | src/Widgets/Playlist.vala 31 | src/Widgets/Radio.vala 32 | src/Widgets/Track.vala 33 | src/Widgets/TrackTimeLine.vala 34 | src/Widgets/Views/AlbumsView.vala 35 | src/Widgets/Views/AlbumView.vala 36 | src/Widgets/Views/ArtistsView.vala 37 | src/Widgets/Views/ArtistView.vala 38 | src/Widgets/Views/AudioCDView.vala 39 | src/Widgets/Views/MobilePhone.vala 40 | src/Widgets/Views/PlaylistsView.vala 41 | src/Widgets/Views/Queue.vala 42 | src/Widgets/Views/RadiosView.vala 43 | src/Widgets/Views/TracksView.vala -------------------------------------------------------------------------------- /data/com.github.artemanufrij.playmymusic.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Melody 3 | GenericName=Melody 4 | Comment=Listen your music 5 | Keywords=music;radio;playlist; 6 | Exec=com.github.artemanufrij.playmymusic %U 7 | Icon=com.github.artemanufrij.playmymusic 8 | Terminal=false 9 | Type=Application 10 | Categories=Audio;Music;Player;AudioVideo;GNOME;GTK; 11 | MimeType=x-content/audio-player;x-content/audio-cdda;application/ogg;application/x-extension-m4a;application/x-extension-mp4;application/x-flac;application/x-ogg;audio/3gpp;audio/aac;audio/ac3;audio/AMR;audio/AMR-WB;audio/basic;audio/flac;audio/midi;audio/mp2;audio/mp4;audio/mpeg;audio/ogg;audio/vnd.rn-realaudio;audio/x-aiff;audio/x-ape;audio/x-flac;audio/x-gsm;audio/x-it;audio/x-m4a;audio/x-matroska;audio/x-mod;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-asf;audio/x-ms-asx;audio/x-ms-wax;audio/x-ms-wma;audio/x-musepack;audio/x-opus+ogg;audio/x-pn-aiff;audio/x-pn-au;audio/x-pn-realaudio;audio/x-pn-realaudio-plugin;audio/x-pn-wav;audio/x-pn-windows-acm;audio/x-realaudio;audio/x-real-audio;audio/x-sbc;audio/x-scpls;audio/x-speex;audio/x-tta;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wav;audio/x-wavpack;audio/x-xm; 12 | X-GNOME-Gettext-Domain=com.github.artemanufrij.playmymusic 13 | X-GNOME-UsesNotifications=true 14 | Actions=Toggle;Next;Prev; 15 | 16 | [Desktop Action Toggle] 17 | Name=Play/Pause 18 | Exec=com.github.artemanufrij.playmymusic --play 19 | Icon=media-playback-start-symbolic 20 | 21 | [Desktop Action Next] 22 | Name=Next 23 | Exec=com.github.artemanufrij.playmymusic --next 24 | Icon=media-skip-forward-symbolic 25 | 26 | [Desktop Action Prev] 27 | Name=Previous 28 | Exec=com.github.artemanufrij.playmymusic --prev 29 | Icon=media-skip-backward-symbolic 30 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | aa 2 | ab 3 | ae 4 | af 5 | ak 6 | am 7 | an 8 | ar 9 | as 10 | ast 11 | av 12 | ay 13 | az 14 | ba 15 | be 16 | bg 17 | bh 18 | bi 19 | bm 20 | bn 21 | bo 22 | br 23 | bs 24 | ca 25 | ce 26 | ch 27 | ckb 28 | co 29 | cr 30 | cs 31 | cu 32 | cv 33 | cy 34 | da 35 | de 36 | dv 37 | dz 38 | ee 39 | el 40 | en_AU 41 | en_CA 42 | en_GB 43 | eo 44 | es 45 | et 46 | eu 47 | fa 48 | ff 49 | fi 50 | fj 51 | fo 52 | fr_CA 53 | fr 54 | fy 55 | ga 56 | gd 57 | gl 58 | gn 59 | gu 60 | gv 61 | ha 62 | he 63 | hi 64 | ho 65 | hr 66 | ht 67 | hu 68 | hy 69 | hz 70 | ia 71 | id 72 | ie 73 | ig 74 | ii 75 | ik 76 | io 77 | is 78 | it 79 | iu 80 | ja 81 | jv 82 | ka 83 | kg 84 | ki 85 | kj 86 | kk 87 | kl 88 | km 89 | kn 90 | ko 91 | kr 92 | ks 93 | ku 94 | kv 95 | kw 96 | ky 97 | la 98 | lb 99 | lg 100 | li 101 | ln 102 | lo 103 | lt 104 | lu 105 | lv 106 | mg 107 | mh 108 | mi 109 | mk 110 | ml 111 | mn 112 | mo 113 | mr 114 | ms 115 | mt 116 | my 117 | na 118 | nb 119 | nd 120 | ne 121 | ng 122 | nl 123 | nn 124 | nr 125 | nv 126 | ny 127 | oc 128 | oj 129 | om 130 | or 131 | os 132 | pa 133 | pi 134 | pl 135 | ps 136 | pt_BR 137 | pt 138 | qu 139 | rm 140 | rn 141 | ro 142 | rue 143 | ru 144 | rw 145 | sa 146 | sc 147 | sd 148 | se 149 | sg 150 | si 151 | sk 152 | sl 153 | sma 154 | sm 155 | sn 156 | so 157 | sq 158 | sr 159 | ss 160 | st 161 | su 162 | sv 163 | sw 164 | ta 165 | te 166 | tg 167 | th 168 | ti 169 | tk 170 | tl 171 | tn 172 | to 173 | tr 174 | ts 175 | tt 176 | tw 177 | ty 178 | ug 179 | uk 180 | ur 181 | uz 182 | ve 183 | vi 184 | vo 185 | wa 186 | wo 187 | xh 188 | yi 189 | yo 190 | za 191 | zh_HK 192 | zh 193 | zh_TW 194 | zu -------------------------------------------------------------------------------- /po/extra/LINGUAS: -------------------------------------------------------------------------------- 1 | aa 2 | ab 3 | ae 4 | af 5 | ak 6 | am 7 | an 8 | ar 9 | as 10 | ast 11 | av 12 | ay 13 | az 14 | ba 15 | be 16 | bg 17 | bh 18 | bi 19 | bm 20 | bn 21 | bo 22 | br 23 | bs 24 | ca 25 | ce 26 | ch 27 | ckb 28 | co 29 | cr 30 | cs 31 | cu 32 | cv 33 | cy 34 | da 35 | de 36 | dv 37 | dz 38 | ee 39 | el 40 | en_AU 41 | en_CA 42 | en_GB 43 | eo 44 | es 45 | et 46 | eu 47 | fa 48 | ff 49 | fi 50 | fj 51 | fo 52 | fr_CA 53 | fr 54 | fy 55 | ga 56 | gd 57 | gl 58 | gn 59 | gu 60 | gv 61 | ha 62 | he 63 | hi 64 | ho 65 | hr 66 | ht 67 | hu 68 | hy 69 | hz 70 | ia 71 | id 72 | ie 73 | ig 74 | ii 75 | ik 76 | io 77 | is 78 | it 79 | iu 80 | ja 81 | jv 82 | ka 83 | kg 84 | ki 85 | kj 86 | kk 87 | kl 88 | km 89 | kn 90 | ko 91 | kr 92 | ks 93 | ku 94 | kv 95 | kw 96 | ky 97 | la 98 | lb 99 | lg 100 | li 101 | ln 102 | lo 103 | lt 104 | lu 105 | lv 106 | mg 107 | mh 108 | mi 109 | mk 110 | ml 111 | mn 112 | mo 113 | mr 114 | ms 115 | mt 116 | my 117 | na 118 | nb 119 | nd 120 | ne 121 | ng 122 | nl 123 | nn 124 | nr 125 | nv 126 | ny 127 | oc 128 | oj 129 | om 130 | or 131 | os 132 | pa 133 | pi 134 | pl 135 | ps 136 | pt_BR 137 | pt 138 | qu 139 | rm 140 | rn 141 | ro 142 | rue 143 | ru 144 | rw 145 | sa 146 | sc 147 | sd 148 | se 149 | sg 150 | si 151 | sk 152 | sl 153 | sma 154 | sm 155 | sn 156 | so 157 | sq 158 | sr 159 | ss 160 | st 161 | su 162 | sv 163 | sw 164 | ta 165 | te 166 | tg 167 | th 168 | ti 169 | tk 170 | tl 171 | tn 172 | to 173 | tr 174 | ts 175 | tt 176 | tw 177 | ty 178 | ug 179 | uk 180 | ur 181 | uz 182 | ve 183 | vi 184 | vo 185 | wa 186 | wo 187 | xh 188 | yi 189 | yo 190 | za 191 | zh_HK 192 | zh 193 | zh_TW 194 | zu -------------------------------------------------------------------------------- /src/Widgets/Views/PlaylistView.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Widgets.Views { 29 | public class PlaylistView : Gtk.FlowBoxChild { 30 | 31 | public Objects.Playlist playlist { get; private set; } 32 | public string title { get { return playlist.title; } } 33 | 34 | Widgets.Playlist content; 35 | 36 | public PlaylistView (Objects.Playlist playlist) { 37 | this.can_focus = false; 38 | this.playlist = playlist; 39 | content = new Widgets.Playlist (playlist); 40 | this.add (content); 41 | } 42 | 43 | public void mark_playing_track (Objects.Track? track) { 44 | content.mark_playing_track (track); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | executable( 2 | meson.project_name(), 3 | 'Application.vala', 4 | 'MainWindow.vala', 5 | 'Settings.vala', 6 | 'Dialogs/AlbumEditor.vala', 7 | 'Dialogs/ArtistEditor.vala', 8 | 'Dialogs/Preferences.vala', 9 | 'Interfaces/Inhibitor.vala', 10 | 'Interfaces/MediaKeys.vala', 11 | 'Interfaces/SoundIndicator.vala', 12 | 'Objects/Album.vala', 13 | 'Objects/Artist.vala', 14 | 'Objects/AudioCD.vala', 15 | 'Objects/MobilePhone.vala', 16 | 'Objects/MobilePhoneMusicFolder.vala', 17 | 'Objects/Playlist.vala', 18 | 'Objects/Radio.vala', 19 | 'Objects/Track.vala', 20 | 'Objects/TracksContainer.vala', 21 | 'Services/DataBaseManager.vala', 22 | 'Services/DeviceManager.vala', 23 | 'Services/LibraryManager.vala', 24 | 'Services/LocalFilesManager.vala', 25 | 'Services/MusicBrainzManager.vala', 26 | 'Services/Player.vala', 27 | 'Services/TagManager.vala', 28 | 'Utils/MusicBrainz.vala', 29 | 'Utils/Utils.vala', 30 | 'Widgets/Album.vala', 31 | 'Widgets/Artist.vala', 32 | 'Widgets/Playlist.vala', 33 | 'Widgets/Radio.vala', 34 | 'Widgets/Track.vala', 35 | 'Widgets/TrackTimeLine.vala', 36 | 'Widgets/Views/AlbumsView.vala', 37 | 'Widgets/Views/AlbumView.vala', 38 | 'Widgets/Views/ArtistsView.vala', 39 | 'Widgets/Views/ArtistView.vala', 40 | 'Widgets/Views/AudioCDView.vala', 41 | 'Widgets/Views/MobilePhone.vala', 42 | 'Widgets/Views/PlaylistsView.vala', 43 | 'Widgets/Views/RadiosView.vala', 44 | 'Widgets/Views/TracksView.vala', 45 | 'Widgets/Views/Queue.vala', 46 | c_args: c_args, 47 | dependencies: [ 48 | dependency('gio-2.0'), 49 | dependency('gobject-2.0'), 50 | dependency('glib-2.0'), 51 | dependency('gtk+-3.0'), 52 | dependency('libsoup-2.4'), 53 | dependency('json-glib-1.0'), 54 | dependency('granite'), 55 | dependency('sqlite3'), 56 | dependency('gstreamer-pbutils-1.0'), 57 | dependency('gstreamer-tag-1.0'), 58 | dependency('taglib_c'), 59 | ], 60 | link_args: ['-lm'], 61 | install : true 62 | ) 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Melody

3 |


A music player for listening to local music files, online radios, and Audio CD's

4 |

Designed for elementary OS

5 |
6 | 7 | [![Build Status](https://travis-ci.org/artemanufrij/playmymusic.svg?branch=master)](https://travis-ci.org/artemanufrij/playmymusic) 8 | 9 | ### Donate 10 | PayPal | LiberaPay | Patreon 11 | 12 |

13 | 14 | Get it on AppCenter 15 | 16 | 17 | Download On Flathub 18 | 19 |

20 | 21 |
22 | 23 | ![screenshot](screenshots/Screenshot.png) 24 | ![screenshot](screenshots/Screenshot_Artists.png) 25 | ![screenshot](screenshots/Screenshot_Tracks.png) 26 | 27 | ## Install from Flatpak 28 | Melody on Flathub: https://flathub.org/apps/details/com.github.artemanufrij.playmymusic 29 | 30 | Install 31 | ``` 32 | flatpak install flathub com.github.artemanufrij.playmymusic 33 | ``` 34 | 35 | Run 36 | ``` 37 | flatpak run com.github.artemanufrij.playmymusic 38 | ``` 39 | 40 | ## Install from Github. 41 | 42 | As first you need elementary SDK 43 | ``` 44 | sudo apt install elementary-sdk 45 | ``` 46 | 47 | Install dependencies 48 | ``` 49 | sudo apt install libsoup2.4-dev libsqlite3-dev libgstreamer-plugins-base1.0-dev libtagc0-dev 50 | ``` 51 | 52 | Clone repository and change directory 53 | ``` 54 | git clone https://github.com/artemanufrij/playmymusic.git 55 | cd playmymusic 56 | ``` 57 | 58 | Compile, install and start Melody on your system 59 | ``` 60 | meson build --prefix=/usr 61 | cd build 62 | sudo ninja install 63 | com.github.artemanufrij.playmymusic 64 | ``` 65 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | com.github.artemanufrij.playmymusic (2.0.0) bionic; urgency=low 2 | 3 | [NEW] 4 | * Queue functionality 5 | * Save and restore last played song and progress 6 | * Switch columns visibility for tracks view 7 | [FIXED] 8 | * Click on headerbar choose current track 9 | * UI: select current track in tracks view 10 | [IMPROVED] 11 | * Reorder controls on header bar 12 | * Drag and Drop behavior for Playlists 13 | 14 | -- Artem Anufrij Tue, 30 Oct 2018 04:53:39 +0100 15 | 16 | com.github.artemanufrij.playmymusic (1.1.2) bionic; urgency=low 17 | 18 | [FIXED] 19 | * Restore correct window size 20 | [TRANSLATION] 21 | * Dutch (by Heimen Stoffels) 22 | * Italian (by papou84) 23 | 24 | -- Artem Anufrij Thu, 25 Oct 2018 04:53:39 +0100 25 | 26 | com.github.artemanufrij.playmymusic (1.1.1) bionic; urgency=low 27 | 28 | [NEW] 29 | * [optional] Delete Playlist if last Track was removed 30 | * Toggle playing by Space Key 31 | * Add Track into Playlist from Trackview 32 | * RadioView: [Ctrl]+[Enter] for accept changes in editor 33 | [FIXED] 34 | * Player doesn't show '0' it the timeline if playing is paused 35 | * Better focus grabing 36 | [IMPROVED] 37 | * Faster start up behaviour 38 | 39 | -- Artem Anufrij Wed, 05 Sep 2018 04:53:39 +0100 40 | 41 | com.github.artemanufrij.playmymusic (0.1.3) xenial; urgency=low 42 | [FIXED] 43 | * Handling with empty ID3 Tags 44 | 45 | -- Artem Anufrij Sat, 07 Oct 2017 04:53:39 +0100 46 | 47 | com.github.artemanufrij.playmymusic (0.1.2) xenial; urgency=low 48 | [FIXED] 49 | * Crash while importing library 50 | * &-char issue 51 | 52 | [NEW] 53 | * Edit Radio Station 54 | * Follow symbol links while scanning library 55 | 56 | [CHANGED] 57 | * Improved behavior on switch to previous Song 58 | * Updated Translation: 59 | * Lithuanian (Translated by: welaq) 60 | -- Artem Anufrij Sat, 07 Oct 2017 04:53:39 +0100 61 | 62 | com.github.artemanufrij.playmymusic (0.1.0) xenial; urgency=low 63 | 64 | * Initial Release. 65 | 66 | -- Artem Anufrij Sun, 01 Oct 2017 04:53:39 +0100 67 | -------------------------------------------------------------------------------- /src/Services/DeviceManager.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Services { 29 | public class DeviceManager : GLib.Object { 30 | static DeviceManager _instance = null; 31 | 32 | public static DeviceManager instance { 33 | get { 34 | if (_instance == null) { 35 | _instance = new DeviceManager (); 36 | } 37 | return _instance; 38 | } 39 | } 40 | 41 | private DeviceManager () { 42 | } 43 | 44 | public signal void audio_cd_added (Volume volume); 45 | public signal void mtp_added (Volume volume); 46 | public signal void audio_cd_removed (Volume volume); 47 | public signal void mtp_removed (Volume volume); 48 | 49 | private GLib.VolumeMonitor monitor; 50 | 51 | construct { 52 | monitor = GLib.VolumeMonitor.get (); 53 | 54 | monitor.volume_added.connect ( 55 | (volume) => { 56 | if (check_for_audio_cd_volume (volume)) { 57 | audio_cd_added (volume); 58 | } else if (check_for_mtp_volume (volume)) { 59 | mtp_added (volume); 60 | } 61 | }); 62 | 63 | monitor.volume_removed.connect ( 64 | (volume) => { 65 | if (check_for_audio_cd_volume (volume)) { 66 | audio_cd_removed (volume); 67 | } else if (check_for_mtp_volume (volume)) { 68 | mtp_removed (volume); 69 | } 70 | }); 71 | } 72 | 73 | public void init () { 74 | var volumes = monitor.get_volumes (); 75 | foreach (var volume in volumes) { 76 | if (check_for_audio_cd_volume (volume)) { 77 | audio_cd_added (volume); 78 | } else if (check_for_mtp_volume (volume)) { 79 | mtp_added (volume); 80 | } 81 | } 82 | } 83 | 84 | private bool check_for_audio_cd_volume (Volume volume) { 85 | File file = volume.get_activation_root (); 86 | return (file != null && file.get_uri ().has_prefix ("cdda://")); 87 | } 88 | 89 | private bool check_for_mtp_volume (Volume volume) { 90 | File file = volume.get_activation_root (); 91 | return (file != null && file.get_uri ().has_prefix ("mtp://")); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Interfaces/MediaKeys.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Interfaces { 29 | [DBus (name = "org.gnome.SettingsDaemon.MediaKeys")] 30 | public interface GnomeMediaKeys : GLib.Object { 31 | public abstract void GrabMediaPlayerKeys (string application, uint32 time) throws Error; 32 | public abstract void ReleaseMediaPlayerKeys (string application) throws Error; 33 | public signal void MediaPlayerKeyPressed (string application, string key); 34 | } 35 | 36 | public class MediaKeyListener : GLib.Object { 37 | public static MediaKeyListener instance { get; private set; } 38 | 39 | PlayMyMusic.Services.LibraryManager library_manager; 40 | 41 | private GnomeMediaKeys? media_keys; 42 | 43 | construct { 44 | library_manager = PlayMyMusic.Services.LibraryManager.instance; 45 | assert (media_keys == null); 46 | 47 | try { 48 | media_keys = Bus.get_proxy_sync (BusType.SESSION, "org.gnome.SettingsDaemon", "/org/gnome/SettingsDaemon/MediaKeys"); 49 | } catch (Error e) { 50 | warning ("Mediakeys error: %s", e.message); 51 | } 52 | 53 | if (media_keys != null) { 54 | media_keys.MediaPlayerKeyPressed.connect (pressed_key); 55 | try { 56 | media_keys.GrabMediaPlayerKeys (PlayMyMusic.PlayMyMusicApp.instance.application_id, (uint32)0); 57 | } 58 | catch (Error err) { 59 | warning ("Could not grab media player keys: %s", err.message); 60 | } 61 | } 62 | } 63 | 64 | private MediaKeyListener (){} 65 | 66 | public static void listen () { 67 | instance = new MediaKeyListener (); 68 | } 69 | 70 | private void pressed_key (dynamic Object bus, string application, string key) { 71 | if (application == (PlayMyMusic.PlayMyMusicApp.instance.application_id)) { 72 | if (key == "Previous") { 73 | library_manager.player.prev (); 74 | } 75 | else if (key == "Play") { 76 | PlayMyMusic.PlayMyMusicApp.instance.mainwindow.toggle_playing (); 77 | } 78 | else if (key == "Next") { 79 | library_manager.player.next (); 80 | } 81 | else if (key == "Pause") { 82 | library_manager.player.pause (); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Settings.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2017 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic { 29 | public enum RepeatMode { 30 | OFF = 0, 31 | ALL = 1, 32 | ONE = 2 33 | } 34 | 35 | public class Settings : Granite.Services.Settings { 36 | private static Settings settings; 37 | public static Settings get_default () { 38 | if (settings == null) { 39 | settings = new Settings (); 40 | } 41 | return settings; 42 | } 43 | public int window_width { get; set; } 44 | public int window_height { get; set; } 45 | public int window_x { get; set; } 46 | public int window_y { get; set; } 47 | public bool window_maximized { get; set; } 48 | public bool shuffle_mode { get; set; } 49 | public RepeatMode repeat_mode { get; set; } 50 | public int last_artist_id { get; set; } 51 | public int last_album_id { get; set; } 52 | public int last_playlist_id { get; set; } 53 | public int last_track_id { get; set; } 54 | public double track_progress { get; set; } 55 | public string track_source { get; set; } 56 | public bool remember_track_progress { get; set; } 57 | public bool play_in_background { get; set; } 58 | public bool sync_files { get; set; } 59 | public string [] artists { get; set; } 60 | public string [] covers { get; set; } 61 | public string library_location { get; set; } 62 | public int view_index { get; set; } 63 | public string [] hidden_tracks_columns { get; set; } 64 | public bool load_content_from_musicbrainz { get; set; } 65 | public bool use_dark_theme { get; set; } 66 | public bool save_custom_covers { get; set; } 67 | public bool save_id3_tags { get; set; } 68 | public bool import_into_library { get; set; } 69 | public bool remove_playlist_if_empty { get; set; } 70 | public int sort_mode_album_view { get; set; } 71 | 72 | private Settings () { 73 | base ("com.github.artemanufrij.playmymusic"); 74 | } 75 | 76 | public void switch_repeat_mode () { 77 | switch (settings.repeat_mode) { 78 | case RepeatMode.ALL: 79 | settings.repeat_mode = RepeatMode.ONE; 80 | break; 81 | case RepeatMode.ONE: 82 | settings.repeat_mode = RepeatMode.OFF; 83 | break; 84 | default: 85 | settings.repeat_mode = RepeatMode.ALL; 86 | break; 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Interfaces/Inhibitor.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2017 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Interfaces { 29 | [DBus (name = "org.freedesktop.ScreenSaver")] 30 | public interface ScreenSaverIface : Object { 31 | public abstract uint32 Inhibit (string app_name, string reason) throws Error; 32 | public abstract void UnInhibit (uint32 cookie) throws Error; 33 | public abstract void SimulateUserActivity () throws Error; 34 | } 35 | 36 | public class Inhibitor : GLib.Object { 37 | const string IFACE = "org.freedesktop.ScreenSaver"; 38 | const string IFACE_PATH = "/ScreenSaver"; 39 | 40 | static Inhibitor? _instance = null; 41 | public static Inhibitor instance { 42 | get { 43 | if (_instance == null) { 44 | _instance = new Inhibitor (); 45 | } 46 | return _instance; 47 | } 48 | } 49 | 50 | uint32? inhibit_cookie = null; 51 | 52 | ScreenSaverIface? screensaver_iface = null; 53 | 54 | uint timer = 0; 55 | 56 | private Inhibitor () { 57 | try { 58 | screensaver_iface = Bus.get_proxy_sync (BusType.SESSION, IFACE, IFACE_PATH, DBusProxyFlags.NONE); 59 | } catch (Error e) { 60 | warning ("Could not start screensaver interface: %s", e.message); 61 | } 62 | } 63 | 64 | public void inhibit () { 65 | if (screensaver_iface != null && timer == 0) { 66 | try { 67 | inhibit_cookie = screensaver_iface.Inhibit ("com.github.artemanufrij.playmyvideos", "Playing movie"); 68 | } catch (Error e) { 69 | warning ("Could not inhibit screen: %s", e.message); 70 | return; 71 | } 72 | 73 | timer = Timeout.add (120000, ()=> { 74 | try { 75 | screensaver_iface.SimulateUserActivity (); 76 | } catch (Error e) { 77 | warning ("Could not simulate user activity: %s", e.message); 78 | } 79 | return true; 80 | }); 81 | } 82 | } 83 | 84 | public void uninhibit () { 85 | if (timer > 0 ) { 86 | Source.remove (timer); 87 | timer = 0; 88 | } 89 | 90 | if (inhibit_cookie != null) { 91 | try { 92 | screensaver_iface.UnInhibit (inhibit_cookie); 93 | inhibit_cookie = null; 94 | } catch (Error e) { 95 | warning ("Could not uninhibit screen: %s", e.message); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Objects/Playlist.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2017 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Objects { 29 | public class Playlist : TracksContainer { 30 | public signal void tracks_resorted (); 31 | public signal void started_init_playing (); 32 | public new GLib.List tracks { 33 | get { 34 | if (_tracks == null) { 35 | _tracks = library_manager.db_manager.get_track_collection (this); 36 | foreach (var track in _tracks) { 37 | track.removed.connect (() => { 38 | _tracks.remove (track); 39 | }); 40 | } 41 | } 42 | return _tracks; 43 | } 44 | } 45 | 46 | construct { 47 | track_removed.connect ((track) => { 48 | this._tracks.remove (track); 49 | if (this.tracks.length () == 0 && settings.remove_playlist_if_empty) { 50 | db_manager.remove_playlist (this); 51 | } 52 | }); 53 | } 54 | 55 | public new void add_track (Track track) { 56 | base.add_track (track); 57 | track.removed.connect (() => { 58 | _tracks.remove (track); 59 | }); 60 | } 61 | 62 | public new bool has_track (int track_id) { 63 | foreach (var track in tracks) { 64 | if (track.ID == track_id) { 65 | return true; 66 | } 67 | } 68 | return false; 69 | } 70 | 71 | public void resort_track (Track track, int new_sort_value) { 72 | if (track.track > new_sort_value) { 73 | foreach (var t in tracks) { 74 | if (t.track < track.track && t.track >= new_sort_value) { 75 | t.track ++; 76 | } 77 | } 78 | track.track = new_sort_value; 79 | } else { 80 | foreach (var t in tracks) { 81 | if (t.track > track.track && t.track < new_sort_value) { 82 | t.track --; 83 | } 84 | } 85 | track.track = new_sort_value - 1; 86 | } 87 | _tracks.sort ((a, b) => { 88 | return a.track - b.track; 89 | }); 90 | tracks_resorted (); 91 | } 92 | 93 | public int get_next_sort_item () { 94 | int return_value = 0; 95 | foreach (var track in tracks) { 96 | if (return_value < track.track) { 97 | return_value = track.track; 98 | } 99 | } 100 | return return_value + 1; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Utils/MusicBrainz.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2017 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Utils { 29 | public class MusicBrainz { 30 | public static string get_artist_id_from_artist_ws_2 (string body) { 31 | var parser = new Json.Parser (); 32 | Json.Node root = null; 33 | try { 34 | parser.load_from_data (body); 35 | root = parser.get_root (); 36 | } catch (Error err) { 37 | warning (err.message); 38 | } 39 | if (root != null) { 40 | if (root.get_object ().has_member ("releases")) { 41 | var releases = root.get_object ().get_member ("releases").get_array (); 42 | if (releases.get_length () > 0) { 43 | var release = releases.get_element (0).get_object (); 44 | if (release.has_member ("artist-credit")) { 45 | var artists = release.get_member ("artist-credit").get_array (); 46 | var artist = artists.get_element (0).get_object (); 47 | return artist.get_member ("artist").get_object ().get_string_member ("id"); 48 | } 49 | } 50 | } 51 | } 52 | return ""; 53 | } 54 | 55 | public static string get_release_id_from_artist_ws_2 (string body) { 56 | var parser = new Json.Parser (); 57 | Json.Node root = null; 58 | try { 59 | parser.load_from_data (body); 60 | root = parser.get_root (); 61 | } catch (Error err) { 62 | warning (err.message); 63 | } 64 | if (root != null) { 65 | if (root.get_object ().has_member ("releases")) { 66 | var releases = root.get_object ().get_member ("releases").get_array (); 67 | if (releases.get_length () > 0) { 68 | var release = releases.get_element (0).get_object (); 69 | return release.get_string_member ("id"); 70 | } 71 | } 72 | } 73 | return ""; 74 | } 75 | 76 | public static string get_large_thumbnail_from_release (string body) { 77 | var parser = new Json.Parser (); 78 | Json.Node root = null; 79 | try { 80 | parser.load_from_data (body); 81 | root = parser.get_root (); 82 | } catch (Error err) { 83 | warning (err.message); 84 | } 85 | if (root != null) { 86 | if (root.get_object ().has_member ("images")) { 87 | var images = root.get_object ().get_member ("images").get_array (); 88 | var image = images.get_element (0).get_object (); 89 | if (image.has_member ("thumbnails")) { 90 | var thumbnail = image.get_member ("thumbnails"); 91 | return thumbnail.get_object ().get_string_member ("large"); 92 | } 93 | } 94 | } 95 | return ""; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Widgets/TrackTimeLine.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2017 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Widgets { 29 | public class TrackTimeLine : Gtk.Grid { 30 | PlayMyMusic.Services.Player player; 31 | public signal void goto_current_track (PlayMyMusic.Objects.Track current_track); 32 | 33 | Gtk.Label playing_track; 34 | Granite.SeekBar timeline; 35 | 36 | PlayMyMusic.Objects.Track current_track; 37 | 38 | construct { 39 | player = PlayMyMusic.Services.Player.instance; 40 | player.current_progress_changed.connect ((progress) => { 41 | timeline.playback_progress = progress; 42 | if (timeline.playback_duration == 0) { 43 | timeline.playback_duration = player.duration / Gst.SECOND; 44 | } 45 | }); 46 | player.current_duration_changed.connect ((duration) => { 47 | timeline.playback_duration = duration / Gst.SECOND; 48 | }); 49 | } 50 | 51 | public TrackTimeLine () { 52 | build_ui (); 53 | } 54 | 55 | private void build_ui () { 56 | this.margin_start = 32; 57 | this.margin_end = 32; 58 | 59 | var content = new Gtk.Grid (); 60 | 61 | playing_track = new Gtk.Label (""); 62 | playing_track.use_markup = true; 63 | playing_track.ellipsize = Pango.EllipsizeMode.END; 64 | playing_track.set_has_window (true); 65 | 66 | var event_box = new Gtk.EventBox (); 67 | event_box.button_release_event.connect (() => { 68 | goto_current_track (current_track); 69 | return false; 70 | }); 71 | event_box.add (playing_track); 72 | content.attach (event_box, 0, 0); 73 | 74 | timeline = new Granite.SeekBar (0); 75 | timeline.scale.change_value.connect ((scroll, new_value) => { 76 | if (scroll == Gtk.ScrollType.JUMP) { 77 | PlayMyMusic.Services.Player.instance.seek_to_progress (new_value); 78 | } 79 | return false; 80 | }); 81 | content.attach (timeline, 0, 1); 82 | 83 | this.add (content); 84 | this.show_all (); 85 | } 86 | 87 | public void set_playing_file (File file) { 88 | current_track = null; 89 | playing_track.label = Utils.markdown_format (file.get_basename ()); 90 | } 91 | 92 | public void set_playing_track (PlayMyMusic.Objects.Track track) { 93 | current_track = track; 94 | if (track.album != null) { 95 | playing_track.label = _("%s from %s by %s").printf (Utils.markdown_format (track.title), 96 | Utils.markdown_format (track.album.title), 97 | Utils.markdown_format (track.album.artist.name)); 98 | } else if (track.audio_cd != null) { 99 | playing_track.label = _("%s from %s by %s").printf (track.title, track.audio_cd.title, track.audio_cd.artist); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Utils/Utils.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Utils { 29 | public static string get_formated_duration (uint64 duration) { 30 | uint seconds = (uint)(duration / 1000000000); 31 | if (seconds < 3600) { 32 | uint minutes = seconds / 60; 33 | seconds -= minutes * 60; 34 | return "%u:%02u".printf (minutes, seconds); 35 | } 36 | 37 | uint hours = seconds / 3600; 38 | seconds -= hours * 3600; 39 | uint minutes = seconds / 60; 40 | seconds -= minutes * 60; 41 | return "%u:%02u:%02u".printf (hours, minutes, seconds); 42 | } 43 | 44 | public static bool is_audio_file (string mime_type) { 45 | return mime_type.has_prefix ("audio/") && !mime_type.contains ("x-mpegurl") && !mime_type.contains ("x-scpls"); 46 | } 47 | 48 | public static void delete_uri_recursive (string uri) { 49 | try { 50 | var directory = File.new_for_uri (uri); 51 | var children = directory.enumerate_children ("standard::*", GLib.FileQueryInfoFlags.NONE); 52 | FileInfo file_info = null; 53 | while ((file_info = children.next_file ()) != null) { 54 | if (file_info.get_file_type () == FileType.DIRECTORY) { 55 | delete_uri_recursive (directory.get_uri () + "/" + file_info.get_name ()); 56 | } else { 57 | var usb = File.new_for_uri (directory.get_uri () + "/" + file_info.get_name ()); 58 | usb.delete (); 59 | } 60 | } 61 | directory.delete (); 62 | } catch (Error err) { 63 | warning (err.message); 64 | } 65 | } 66 | 67 | public static string markdown_format (string input) { 68 | return input.replace ("&", "&").replace ("<", "<").replace (">", ">"); 69 | } 70 | 71 | public static void set_custom_css_style (Gdk.Screen screen) { 72 | Granite.Widgets.Utils.set_theming_for_screen ( 73 | screen, 74 | """ 75 | .artist-title { 76 | color: #fff; 77 | text-shadow: 0px 1px 2px alpha (#000, 1); 78 | } 79 | .artist-sub-title { 80 | color: #fff; 81 | text-shadow: 0px 1px 2px alpha (#000, 1); 82 | } 83 | .playlist-tracks { 84 | background: transparent; 85 | } 86 | .mode_button_split { 87 | border-left-width: 1px; 88 | } 89 | .artist-tracks { 90 | background: rgba (245, 245, 245, 0.75); 91 | } 92 | .artist-tracks-dark { 93 | background: rgba (54, 59, 62, 0.75); 94 | } 95 | .custom_titlebar { 96 | padding-top: 0px; 97 | padding-bottom: 0px; 98 | } 99 | .track-drag-begin { 100 | border-top: 1px solid #666666; 101 | } 102 | .mobile-close-button { 103 | padding: 3px; 104 | opacity: 0.75; 105 | } 106 | """, 107 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Widgets/Radio.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Widgets { 29 | public class Radio : Gtk.FlowBoxChild { 30 | PlayMyMusic.Services.LibraryManager library_manager; 31 | 32 | public signal void edit_request (); 33 | 34 | public PlayMyMusic.Objects.Radio radio { get; private set; } 35 | public string title { get { return radio.title; } } 36 | 37 | Gtk.Image cover; 38 | Gtk.Menu menu; 39 | Gtk.Label station_title; 40 | 41 | construct { 42 | library_manager = PlayMyMusic.Services.LibraryManager.instance; 43 | } 44 | 45 | public Radio (PlayMyMusic.Objects.Radio radio) { 46 | this.radio = radio; 47 | this.radio.cover_changed.connect (() => { 48 | if (this.radio.cover != null) { 49 | cover.pixbuf = this.radio.cover; 50 | } 51 | }); 52 | this.radio.removed.connect (() => { 53 | this.destroy (); 54 | }); 55 | 56 | this.radio.notify["title"].connect (() => { 57 | station_title.label = ("%s").printf(this.radio.title); 58 | }); 59 | 60 | this.radio.notify["url"].connect (() => { 61 | this.tooltip_text = this.radio.url; 62 | }); 63 | 64 | build_ui (); 65 | } 66 | 67 | private void build_ui () { 68 | var event_box = new Gtk.EventBox (); 69 | event_box.button_press_event.connect (show_context_menu); 70 | 71 | var content = new Gtk.Grid (); 72 | content.halign = Gtk.Align.CENTER; 73 | content.row_spacing = 6; 74 | content.margin = 12; 75 | event_box.add (content); 76 | 77 | cover = new Gtk.Image (); 78 | cover.get_style_context ().add_class ("card"); 79 | cover.halign = Gtk.Align.CENTER; 80 | if (this.radio.cover == null) { 81 | cover.set_from_icon_name ("network-cellular-connected-symbolic", Gtk.IconSize.DIALOG); 82 | cover.height_request = 64; 83 | cover.width_request = 64; 84 | } else { 85 | cover.pixbuf = this.radio.cover; 86 | } 87 | content.attach (cover, 0, 0); 88 | 89 | station_title = new Gtk.Label (("%s").printf(Utils.markdown_format (radio.title))); 90 | station_title.use_markup = true; 91 | station_title.halign = Gtk.Align.CENTER; 92 | content.attach (station_title, 0, 1); 93 | 94 | menu = new Gtk.Menu (); 95 | var menu_new_cover = new Gtk.MenuItem.with_label (_("Edit Radio Station…")); 96 | menu_new_cover.activate.connect (() => { 97 | edit_request (); 98 | }); 99 | menu.append (menu_new_cover); 100 | 101 | menu.append (new Gtk.SeparatorMenuItem ()); 102 | 103 | var menu_remove = new Gtk.MenuItem.with_label (_("Remove Radio Station")); 104 | menu_remove.activate.connect (() => { 105 | library_manager.remove_radio_station (this.radio); 106 | }); 107 | menu.append (menu_remove); 108 | 109 | menu.show_all (); 110 | 111 | this.tooltip_text = radio.url; 112 | this.add (event_box); 113 | } 114 | 115 | private bool show_context_menu (Gtk.Widget sender, Gdk.EventButton evt) { 116 | if (evt.type == Gdk.EventType.BUTTON_PRESS && evt.button == 3) { 117 | menu.popup_at_pointer (null); 118 | return true; 119 | } 120 | return false; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /data/icons/symbolic/playlist-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | 39 | 49 | 51 | 55 | 59 | 60 | 61 | 63 | 64 | 66 | image/svg+xml 67 | 69 | 70 | 71 | 72 | 73 | 75 | 81 | 82 | 89 | 96 | 103 | 110 | 117 | 124 | 131 | 138 | 139 | -------------------------------------------------------------------------------- /src/Objects/Track.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Objects { 29 | public class Track : GLib.Object { 30 | Services.LibraryManager library_manager; 31 | 32 | public signal void removed (); 33 | public signal void album_changed (Album album); 34 | public signal void tags_saved (); 35 | 36 | Album ? _album = null; 37 | public Album album { 38 | get { 39 | if (_album == null) { 40 | _album = library_manager.db_manager.get_album_by_track_id (this.ID); 41 | } 42 | return _album; 43 | } 44 | } 45 | 46 | Playlist ? _playlist = null; 47 | public Playlist ? playlist { 48 | get { 49 | return _playlist; 50 | } 51 | } 52 | 53 | public AudioCD _audio_cd = null; 54 | public AudioCD audio_cd { 55 | get { 56 | return _audio_cd; 57 | } 58 | } 59 | 60 | public int ID { get; set; default = 0; } 61 | public string title { get; set; default = ""; } 62 | public string genre { get; set; default = ""; } 63 | public int track { get; set; default = 0; } 64 | public int disc { get; set; default = 0; } 65 | public uint64 duration { get; set; default = 0; } 66 | 67 | //LOCATION 68 | string _uri = ""; 69 | public string uri { 70 | get { 71 | return _uri; 72 | } set { 73 | _uri = value; 74 | } 75 | } 76 | 77 | public signal void path_not_found (); 78 | 79 | construct { 80 | library_manager = Services.LibraryManager.instance; 81 | removed.connect ( 82 | () => { 83 | if (album != null) { 84 | album.track_removed (this); 85 | album.artist.track_removed (this); 86 | } 87 | if (playlist != null) { 88 | playlist.track_removed (this); 89 | } 90 | }); 91 | } 92 | 93 | public Track (TracksContainer ? container = null) { 94 | if (container != null && container is Album) { 95 | set_album (container as Album); 96 | } else if (container != null && container is Playlist) { 97 | set_playlist (container as Playlist); 98 | } else if (container != null && container is AudioCD) { 99 | set_audio_cd (container as AudioCD); 100 | } 101 | } 102 | 103 | public void set_album (Album album) { 104 | _album = album; 105 | album_changed (album); 106 | } 107 | 108 | public void set_playlist (Playlist playlist) { 109 | _playlist = playlist; 110 | } 111 | 112 | private void set_audio_cd (AudioCD audio_cd) { 113 | _audio_cd = audio_cd; 114 | } 115 | 116 | public bool file_exists () { 117 | bool return_value = true; 118 | var file = File.new_for_uri (uri); 119 | if (!file.query_exists ()) { 120 | path_not_found (); 121 | return_value = false; 122 | } 123 | file.dispose (); 124 | return return_value; 125 | } 126 | 127 | public void save_id3_tags () { 128 | var path = File.new_for_uri (uri); 129 | 130 | var file = new TagLib.File (path.get_path ()); 131 | if (file.tag.album == this.album.title && file.tag.artist == this.album.artist.name && file.tag.year == this.album.year && file.tag.genre == genre) { 132 | return; 133 | } 134 | 135 | file.tag.album = this.album.title; 136 | file.tag.artist = this.album.artist.name; 137 | file.tag.year = this.album.year; 138 | file.tag.genre = genre; 139 | if (file.save ()) { 140 | tags_saved (); 141 | } 142 | path.dispose (); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /data/icons/16/com.github.artemanufrij.playmymusic.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 | -------------------------------------------------------------------------------- /src/Services/LocalFilesManager.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Services { 29 | public class LocalFilesManager : GLib.Object { 30 | static LocalFilesManager _instance = null; 31 | public static LocalFilesManager instance { 32 | get { 33 | if (_instance == null) { 34 | _instance = new LocalFilesManager (); 35 | } 36 | return _instance; 37 | } 38 | } 39 | 40 | public signal void scan_started (); 41 | public signal void found_music_file (string path); 42 | 43 | private LocalFilesManager () { } 44 | 45 | public void scan (string path) { 46 | scan_started (); 47 | scan_local_files (path); 48 | } 49 | 50 | private void scan_local_files (string uri) { 51 | new Thread ("scan_local_files", () => { 52 | File directory = File.new_for_uri (uri.replace ("#", "%23")); 53 | stdout.printf ("%s\n", directory.get_uri ()); 54 | try { 55 | var children = directory.enumerate_children ("standard::*," + FileAttribute.STANDARD_CONTENT_TYPE + "," + FileAttribute.STANDARD_IS_HIDDEN + "," + FileAttribute.STANDARD_IS_SYMLINK + "," + FileAttribute.STANDARD_SYMLINK_TARGET, GLib.FileQueryInfoFlags.NONE); 56 | FileInfo file_info = null; 57 | 58 | while ((file_info = children.next_file ()) != null) { 59 | if (file_info.get_is_hidden ()) { 60 | continue; 61 | } 62 | 63 | if (file_info.get_is_symlink ()) { 64 | string target = file_info.get_symlink_target (); 65 | var symlink = File.new_for_path (target); 66 | var file_type = symlink.query_file_type (0); 67 | if (file_type == FileType.DIRECTORY) { 68 | scan_local_files (target); 69 | } 70 | } else if (file_info.get_file_type () == FileType.DIRECTORY) { 71 | // Without usleep it crashes on smb:// protocol and mounted devices 72 | //if (!directory.get_uri ().has_prefix ("file://")) { 73 | Thread.usleep (1000000); 74 | //} 75 | scan_local_files (directory.get_uri () + "/" + file_info.get_name ()); 76 | } else { 77 | string mime_type = file_info.get_content_type (); 78 | if (Utils.is_audio_file (mime_type)) { 79 | found_music_file (directory.get_uri () + "/" + file_info.get_name ().replace ("#", "%23")); 80 | } 81 | } 82 | } 83 | children.close (); 84 | children.dispose (); 85 | } catch (Error err) { 86 | warning ("%s\n%s", err.message, uri); 87 | } 88 | directory.dispose (); 89 | return null; 90 | }); 91 | } 92 | 93 | public string get_file_uri (string path, string parent = "") { 94 | string file_uri = path; 95 | if (!path.has_prefix ("/")) { 96 | file_uri = "%s/%s".printf(parent, path); 97 | stdout.printf ("%s seems not an absolute path: adding parent (%s)\n", path, file_uri); 98 | } 99 | 100 | File path_file = File.new_for_path(file_uri); 101 | try { 102 | FileInfo path_file_info = path_file.query_info ("%s,%s".printf (FileAttribute.STANDARD_SYMLINK_TARGET, FileAttribute.STANDARD_IS_SYMLINK), FileQueryInfoFlags.NONE); 103 | if (path_file_info.get_is_symlink ()) { 104 | stdout.printf ("%s is symlink: getting target...\n", file_uri); 105 | file_uri = path_file_info.get_symlink_target (); 106 | file_uri = (!file_uri.has_prefix ("/")) ? "%s/%s".printf(parent, file_uri) : file_uri; 107 | stdout.printf ("Target: %s\n", file_uri); 108 | path_file = File.new_for_path (file_uri); 109 | } 110 | } catch (Error error) { 111 | // cannot parse file symlinks informations 112 | // ignore it 113 | } 114 | 115 | stdout.printf ("Returning absolute path: %s\n", path_file.resolve_relative_path (".").get_path ()); 116 | return path_file.resolve_relative_path (".").get_path (); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Widgets/Views/Queue.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2018-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Widgets.Views { 29 | public class Queue : Gtk.Grid { 30 | public signal void moved_to_playlist (); 31 | Services.LibraryManager library_manager; 32 | Services.Player player; 33 | 34 | public Objects.Playlist playlist { get; private set; } 35 | Widgets.Playlist queue; 36 | 37 | Gtk.Grid content; 38 | Gtk.Label message; 39 | 40 | construct { 41 | library_manager = Services.LibraryManager.instance; 42 | player = library_manager.player; 43 | player.state_changed.connect ((state) => { 44 | if (state == Gst.State.PLAYING 45 | && player.play_mode == Services.PlayMode.PLAYLIST 46 | && player.current_track.playlist.ID == playlist.ID) { 47 | queue.mark_playing_track (player.current_track); 48 | } 49 | }); 50 | } 51 | 52 | public Queue () { 53 | playlist = library_manager.db_manager.get_queue (); 54 | playlist.track_added.connect (() => { 55 | set_visibility (); 56 | }); 57 | playlist.track_removed.connect (() => { 58 | set_visibility (); 59 | }); 60 | playlist.started_init_playing.connect (() => { 61 | queue.mark_playing_track (playlist.get_first_track ()); 62 | }); 63 | build_ui (); 64 | } 65 | 66 | private void build_ui () { 67 | queue = new Widgets.Playlist (playlist, TrackStyle.QUEUE); 68 | queue.height_request = 320; 69 | 70 | var controls = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); 71 | controls.halign = Gtk.Align.FILL; 72 | 73 | var button_trash_queue = new Gtk.Button.from_icon_name ("edit-delete-symbolic"); 74 | button_trash_queue.tooltip_text = _("Clear the Queue"); 75 | button_trash_queue.margin = 6; 76 | button_trash_queue.margin_end = 0; 77 | button_trash_queue.clicked.connect (() => { 78 | while (playlist.tracks.first () != null) { 79 | library_manager.remove_track_from_playlist (playlist.tracks.first ().data); 80 | } 81 | }); 82 | 83 | var button_create_playlist = new Gtk.Button.from_icon_name ("playlist-symbolic"); 84 | button_create_playlist.tooltip_text = _("Create new Playlist based on this Queue"); 85 | button_create_playlist.margin = 6; 86 | button_create_playlist.clicked.connect (() => { 87 | var new_playlist = library_manager.create_new_playlist (); 88 | var date_time = new GLib.DateTime.now_local ().format ("%Y-%m-%d %H:%M:%S"); 89 | new_playlist.title = _ ("Queue from %s").printf (date_time); 90 | library_manager.db_manager.update_playlist (new_playlist); 91 | 92 | foreach (var track in playlist.tracks) { 93 | library_manager.add_track_into_playlist (new_playlist, track.ID); 94 | } 95 | 96 | moved_to_playlist (); 97 | }); 98 | 99 | var button_export_playlist = new Gtk.Button.from_icon_name ("document-export-symbolic"); 100 | button_export_playlist.tooltip_text = _("Export Queue…"); 101 | button_export_playlist.margin = 6; 102 | button_export_playlist.clicked.connect (() => { 103 | var date_time = new GLib.DateTime.now_local ().format ("%Y-%m-%d %H:%M:%S"); 104 | library_manager.export_playlist (playlist, _ ("Queue from %s").printf (date_time)); 105 | }); 106 | 107 | controls.pack_start (button_export_playlist, false, false); 108 | 109 | controls.pack_end (button_create_playlist, false, false); 110 | controls.pack_end (button_trash_queue, false, false); 111 | 112 | content = new Gtk.Grid (); 113 | 114 | content.attach (queue, 0, 0); 115 | content.attach (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), 0, 1); 116 | content.attach (controls, 0, 2); 117 | 118 | message = new Gtk.Label (_("Queue is empty")); 119 | message.get_style_context ().add_class ("h3"); 120 | message.margin = 12; 121 | 122 | this.add (content); 123 | this.add (message); 124 | 125 | this.show_all (); 126 | set_visibility (); 127 | } 128 | 129 | private void set_visibility () { 130 | if (playlist.has_available_tracks ()) { 131 | content.show (); 132 | message.hide (); 133 | } else { 134 | content.hide (); 135 | message.show (); 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/Dialogs/ArtistEditor.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Dialogs { 29 | public class ArtistEditor : Gtk.Dialog { 30 | PlayMyMusic.Services.LibraryManager library_manager; 31 | PlayMyMusic.Services.DataBaseManager db_manager; 32 | PlayMyMusic.Settings settings; 33 | Objects.Artist artist; 34 | 35 | Gtk.Image cover; 36 | Gtk.Entry name_entry; 37 | 38 | bool cover_changed = false; 39 | string? new_cover_path = null; 40 | 41 | construct { 42 | library_manager = PlayMyMusic.Services.LibraryManager.instance; 43 | db_manager = PlayMyMusic.Services.DataBaseManager.instance; 44 | settings = PlayMyMusic.Settings.get_default (); 45 | } 46 | 47 | public ArtistEditor (Gtk.Window parent, Objects.Artist artist) { 48 | Object ( 49 | transient_for: parent 50 | ); 51 | this.artist = artist; 52 | build_ui (); 53 | 54 | this.response.connect ((source, response_id) => { 55 | switch (response_id) { 56 | case Gtk.ResponseType.ACCEPT: 57 | save (); 58 | break; 59 | } 60 | }); 61 | this.key_press_event.connect ((event) => { 62 | if ((event.keyval == Gdk.Key.Return || event.keyval == Gdk.Key.KP_Enter) && Gdk.ModifierType.CONTROL_MASK in event.state) { 63 | save (); 64 | } 65 | return false; 66 | }); 67 | } 68 | 69 | private void build_ui () { 70 | this.resizable = false; 71 | var content = get_content_area () as Gtk.Box; 72 | 73 | var grid = new Gtk.Grid (); 74 | grid.column_spacing = 12; 75 | grid.row_spacing = 12; 76 | grid.margin = 6; 77 | grid.margin_top = 0; 78 | 79 | var event_box = new Gtk.EventBox (); 80 | 81 | cover = new Gtk.Image (); 82 | cover.tooltip_text = _("Click to choose a new cover…"); 83 | event_box.button_press_event.connect ((event) => { 84 | new_cover_path = library_manager.choose_new_cover (); 85 | if (new_cover_path != null) { 86 | try { 87 | cover.pixbuf = library_manager.align_and_scale_pixbuf (new Gdk.Pixbuf.from_file (new_cover_path), 256); 88 | cover_changed = true; 89 | } catch (Error err) { 90 | warning (err.message); 91 | } 92 | } 93 | return false; 94 | }); 95 | if (artist.cover == null) { 96 | cover.set_from_icon_name ("audio-x-generic-symbolic", Gtk.IconSize.DIALOG); 97 | cover.height_request = 256; 98 | cover.width_request = 256; 99 | } else { 100 | cover.pixbuf = artist.cover; 101 | } 102 | 103 | event_box.add (cover); 104 | 105 | name_entry = new Gtk.Entry (); 106 | name_entry.get_style_context ().add_class ("h3"); 107 | name_entry.text = artist.name; 108 | 109 | grid.attach (event_box, 0, 0, 2, 1); 110 | grid.attach (name_entry, 0, 1, 2, 1); 111 | 112 | content.pack_start (grid, false, false, 0); 113 | 114 | var save_button = this.add_button (_("Save"), Gtk.ResponseType.ACCEPT) as Gtk.Button; 115 | save_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); 116 | this.show_all (); 117 | } 118 | 119 | private void save () { 120 | var new_name = name_entry.text.strip (); 121 | 122 | if (new_name == artist.name && !cover_changed) { 123 | return; 124 | } 125 | 126 | var artist_exists = db_manager.get_artist_by_name (new_name); 127 | if (artist_exists == null || artist_exists.ID == artist.ID) { 128 | artist.name = new_name; 129 | db_manager.update_artist (artist); 130 | if (cover_changed) { 131 | artist.set_new_cover (cover.pixbuf, 256); 132 | if (settings.save_custom_covers) { 133 | artist.set_custom_cover_file (new_cover_path); 134 | } 135 | } 136 | } else { 137 | GLib.List artists = new GLib.List (); 138 | artists.append (artist); 139 | artist_exists.merge (artists); 140 | if (cover_changed) { 141 | artist_exists.set_new_cover (cover.pixbuf, 256); 142 | if (settings.save_custom_covers) { 143 | artist_exists.set_custom_cover_file (new_cover_path); 144 | } 145 | } 146 | } 147 | this.destroy (); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /schemas/com.github.artemanufrij.playmymusic.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 1024 10 | The saved width of the window. 11 | The saved width of the window. 12 | 13 | 14 | 720 15 | The saved height of the window. 16 | The saved height of the window. 17 | 18 | 19 | -1 20 | The saved x-position of the window. 21 | The saved x-position of the window. 22 | 23 | 24 | -1 25 | The saved y-position of the window. 26 | The saved y-position of the window. 27 | 28 | 29 | 0 30 | Last Artist ID. 31 | Last Artist ID. 32 | 33 | 34 | 0 35 | Last Album ID. 36 | Last Album ID. 37 | 38 | 39 | 0 40 | Last Playlist ID. 41 | Last Playlist ID. 42 | 43 | 44 | 0 45 | Current track position 46 | Current track position. 47 | 48 | 49 | 0 50 | Current track position 51 | Current track position. 52 | 53 | 54 | "" 55 | Track source album or artist 56 | Track source album or artist. 57 | 58 | 59 | true 60 | Remember Track progress 61 | Remember Track progress. 62 | 63 | 64 | false 65 | Maximized window. 66 | Maximized window. 67 | 68 | 69 | false 70 | Shuffle mode. 71 | Shuffle mode. 72 | 73 | 74 | "off" 75 | Repeat mode. 76 | Repeat mode. 77 | 78 | 79 | false 80 | Play in background if closed. 81 | Play in background if closed. 82 | 83 | 84 | true 85 | Look for new music files on start up. 86 | Look for new music files on start up. 87 | 88 | 89 | ['artist.jpg','Artist.jpg','artist.png','Artist.png','interpreter.jpg','Interpreter.jpg','interpreter.png','Interpreter.png','band.jpg','Band.jpg','band.png','Band.png'] 90 | Artwork file names. 91 | Artwork file names. 92 | 93 | 94 | ['cover.jpg','Cover.jpg','cover.png','Cover.png','album.jpg','Album.jpg','album.png','Album.png','folder.jpg','Folder.jpg','folder.png','Folder.png','front.jpg','Front.jpg','front.png','Front.png'] 95 | Cover file names. 96 | Cover file names. 97 | 98 | 99 | "" 100 | Default library location. 101 | Default library location. 102 | 103 | 104 | 0 105 | Last choosed view. 106 | Last choosed view. 107 | 108 | 109 | [] 110 | Hidden columns inside tracks view. 111 | Hidden columns inside tracks view. 112 | 113 | 114 | true 115 | Load Artwork and Cover from MusicBrainz. 116 | Load Artwork and Cover from MusicBrainz. 117 | 118 | 119 | false 120 | Use Dark Theme. 121 | Use Dark Theme. 122 | 123 | 124 | false 125 | Save custom Cover in Music folder. 126 | Save custom Cover in Music folder. 127 | 128 | 129 | false 130 | Save changes into file. 131 | Save changes into file. 132 | 133 | 134 | false 135 | Import content into library. 136 | Import content into library. 137 | 138 | 139 | true 140 | Remove Playlist if last Track was removed. 141 | Remove Playlist if last Track was removed. 142 | 143 | 144 | 1 145 | Sort Mode for Album view. 146 | Sort Mode for Album view. 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/Dialogs/Preferences.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Dialogs { 29 | public class Preferences : Gtk.Dialog { 30 | PlayMyMusic.Settings settings; 31 | 32 | construct { 33 | settings = PlayMyMusic.Settings.get_default (); 34 | } 35 | 36 | public Preferences (Gtk.Window parent) { 37 | Object ( 38 | transient_for: parent, 39 | deletable: false, 40 | resizable: false 41 | ); 42 | build_ui (); 43 | 44 | this.response.connect ((source, response_id) => { 45 | switch (response_id) { 46 | case Gtk.ResponseType.CLOSE: 47 | destroy (); 48 | break; 49 | } 50 | }); 51 | } 52 | 53 | private void build_ui () { 54 | title = _("Preferences"); 55 | var content = get_content_area () as Gtk.Box; 56 | 57 | var grid = new Gtk.Grid (); 58 | grid.column_spacing = 12; 59 | grid.row_spacing = 12; 60 | grid.margin = 12; 61 | 62 | var play_in_background = new Gtk.Switch (); 63 | play_in_background.active = settings.play_in_background; 64 | play_in_background.notify["active"].connect (() => { settings.play_in_background = play_in_background.active; }); 65 | 66 | var sync_files = new Gtk.Switch (); 67 | sync_files.active = settings.sync_files; 68 | sync_files.notify["active"].connect (() => { settings.sync_files = sync_files.active; }); 69 | 70 | var load_content = new Gtk.Switch (); 71 | load_content.active = settings.load_content_from_musicbrainz; 72 | load_content.notify["active"].connect (() => { settings.load_content_from_musicbrainz = load_content.active; }); 73 | 74 | var save_custom_covers = new Gtk.Switch (); 75 | save_custom_covers.active = settings.save_custom_covers; 76 | save_custom_covers.notify["active"].connect (() => { settings.save_custom_covers = save_custom_covers.active; }); 77 | 78 | var save_id3_tags = new Gtk.Switch (); 79 | save_id3_tags.active = settings.save_id3_tags; 80 | save_id3_tags.notify["active"].connect (() => { settings.save_id3_tags = save_id3_tags.active; }); 81 | 82 | var import_into_library = new Gtk.Switch (); 83 | import_into_library.active = settings.import_into_library; 84 | import_into_library.notify["active"].connect (() => { settings.import_into_library = import_into_library.active; }); 85 | 86 | var remove_playlist_if_empty = new Gtk.Switch(); 87 | remove_playlist_if_empty.active = settings.remove_playlist_if_empty; 88 | remove_playlist_if_empty.notify["active"].connect (() => { settings.remove_playlist_if_empty = remove_playlist_if_empty.active; }); 89 | 90 | var remember_track_progress = new Gtk.Switch (); 91 | remember_track_progress.active = settings.remember_track_progress; 92 | remember_track_progress.notify["active"].connect (() => { settings.remember_track_progress = remember_track_progress.active; }); 93 | 94 | grid.attach (label_generator (_ ("Play in background if closed")), 0, 0); 95 | grid.attach (play_in_background, 1, 0); 96 | grid.attach (label_generator (_ ("Sync files on start up")), 0, 1); 97 | grid.attach (sync_files, 1, 1); 98 | grid.attach (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), 0, 2, 2, 1); 99 | grid.attach (label_generator (_ ("Load Content from MusicBrainz")), 0, 3); 100 | grid.attach (load_content, 1, 3); 101 | grid.attach (label_generator (_ ("Save custom Covers in Library folder")), 0, 4); 102 | grid.attach (save_custom_covers, 1, 4); 103 | grid.attach (label_generator (_ ("Save changes into ID3-Tag")), 0, 5); 104 | grid.attach (save_id3_tags, 1, 5); 105 | grid.attach (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), 0, 6, 2, 1); 106 | grid.attach (label_generator (_ ("Copy Imported Files into Library")), 0, 7); 107 | grid.attach (import_into_library, 1, 7); 108 | grid.attach (label_generator (_ ("Remove Playlist if last Track was removed")), 0, 8); 109 | grid.attach (remove_playlist_if_empty, 1, 8); 110 | grid.attach (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), 0, 9, 2, 1); 111 | grid.attach (label_generator (_ ("Remember Track progress")), 0, 10); 112 | grid.attach (remember_track_progress, 1, 10); 113 | 114 | content.pack_start (grid, false, false, 0); 115 | 116 | this.add_button (_ ("Close"), Gtk.ResponseType.CLOSE); 117 | this.show_all (); 118 | } 119 | 120 | private Gtk.Label label_generator (string content) { 121 | return new Gtk.Label (content) { 122 | halign = Gtk.Align.START, 123 | hexpand = true 124 | }; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Dialogs/AlbumEditor.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Dialogs { 29 | public class AlbumEditor : Gtk.Dialog { 30 | PlayMyMusic.Services.LibraryManager library_manager; 31 | PlayMyMusic.Services.DataBaseManager db_manager; 32 | PlayMyMusic.Settings settings; 33 | Objects.Album album; 34 | 35 | Gtk.Image cover; 36 | Gtk.Entry title_entry; 37 | Gtk.Entry year_entry; 38 | 39 | bool cover_changed = false; 40 | string? new_cover_path = null; 41 | 42 | construct { 43 | library_manager = PlayMyMusic.Services.LibraryManager.instance; 44 | db_manager = PlayMyMusic.Services.DataBaseManager.instance; 45 | settings = PlayMyMusic.Settings.get_default (); 46 | } 47 | 48 | public AlbumEditor (Gtk.Window parent, Objects.Album album) { 49 | Object ( 50 | transient_for: parent 51 | ); 52 | this.album = album; 53 | build_ui (); 54 | 55 | this.response.connect ((source, response_id) => { 56 | switch (response_id) { 57 | case Gtk.ResponseType.ACCEPT: 58 | save (); 59 | break; 60 | } 61 | }); 62 | this.key_press_event.connect ((event) => { 63 | if ((event.keyval == Gdk.Key.Return || event.keyval == Gdk.Key.KP_Enter) && Gdk.ModifierType.CONTROL_MASK in event.state) { 64 | save (); 65 | } 66 | return false; 67 | }); 68 | } 69 | 70 | private void build_ui () { 71 | this.resizable = false; 72 | var content = get_content_area () as Gtk.Box; 73 | 74 | var grid = new Gtk.Grid (); 75 | grid.column_spacing = 12; 76 | grid.row_spacing = 12; 77 | grid.margin = 6; 78 | grid.margin_top = 0; 79 | 80 | var event_box = new Gtk.EventBox (); 81 | 82 | cover = new Gtk.Image (); 83 | cover.tooltip_text = _("Click to choose a new cover…"); 84 | event_box.button_press_event.connect ((event) => { 85 | new_cover_path = library_manager.choose_new_cover (); 86 | if (new_cover_path != null) { 87 | try { 88 | cover.pixbuf = library_manager.align_and_scale_pixbuf (new Gdk.Pixbuf.from_file (new_cover_path), 256); 89 | cover_changed = true; 90 | } catch (Error err) { 91 | warning (err.message); 92 | } 93 | } 94 | return false; 95 | }); 96 | if (album.cover == null) { 97 | cover.set_from_icon_name ("audio-x-generic-symbolic", Gtk.IconSize.DIALOG); 98 | cover.height_request = 256; 99 | cover.width_request = 256; 100 | } else { 101 | cover.pixbuf = album.cover; 102 | } 103 | 104 | event_box.add (cover); 105 | 106 | title_entry = new Gtk.Entry (); 107 | title_entry.get_style_context ().add_class ("h3"); 108 | title_entry.text = album.title; 109 | 110 | var year_label = new Gtk.Label (_("Year")); 111 | year_label.halign = Gtk.Align.END; 112 | year_entry = new Gtk.Entry (); 113 | year_entry.text = album.year.to_string (); 114 | 115 | grid.attach (event_box, 0, 0, 2, 1); 116 | grid.attach (title_entry, 0, 1, 2, 1); 117 | grid.attach (year_label, 0, 2); 118 | grid.attach (year_entry, 1, 2); 119 | content.pack_start (grid, false, false, 0); 120 | 121 | var save_button = this.add_button (_("Save"), Gtk.ResponseType.ACCEPT) as Gtk.Button; 122 | save_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); 123 | this.show_all (); 124 | } 125 | 126 | private void save () { 127 | var new_title = title_entry.text.strip (); 128 | var new_year = int.parse (year_entry.text); 129 | 130 | if (new_title == album.title && new_year == album.year && !cover_changed) { 131 | return; 132 | } 133 | 134 | var album_exists = album.artist.get_album_by_title (new_title); 135 | if (album_exists == null || album_exists.ID == album.ID) { 136 | album.title = new_title; 137 | album.year = new_year; 138 | db_manager.update_album (album); 139 | if (cover_changed) { 140 | album.set_new_cover (cover.pixbuf, 256); 141 | if (settings.save_custom_covers) { 142 | album.set_custom_cover_file (new_cover_path); 143 | } 144 | } 145 | } else { 146 | GLib.List albums = new GLib.List (); 147 | albums.append (album); 148 | album_exists.merge (albums); 149 | if (cover_changed) { 150 | album_exists.set_new_cover (cover.pixbuf, 256); 151 | if (settings.save_custom_covers) { 152 | album_exists.set_custom_cover_file (new_cover_path); 153 | } 154 | } 155 | } 156 | 157 | this.destroy (); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Objects/MobilePhoneMusicFolder.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2017 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Objects { 29 | public class MobilePhoneMusicFolder : Granite.Widgets.SourceList.ExpandableItem, Granite.Widgets.SourceListSortable, Granite.Widgets.SourceListDragDest { 30 | PlayMyMusic.Services.LibraryManager library_manager; 31 | 32 | public signal void subfolder_created (File file); 33 | public signal void subfolder_deleted (); 34 | 35 | public File file { get; private set; } 36 | 37 | construct { 38 | library_manager = PlayMyMusic.Services.LibraryManager.instance; 39 | } 40 | 41 | public MobilePhoneMusicFolder (string uri) { 42 | file = File.new_for_uri (uri); 43 | this.name = file.get_basename (); 44 | get_subfolders (); 45 | } 46 | 47 | private void get_subfolders () { 48 | try { 49 | var children = file.enumerate_children ("standard::*", GLib.FileQueryInfoFlags.NONE); 50 | FileInfo file_info = null; 51 | while ((file_info = children.next_file ()) != null) { 52 | if (file_info.get_file_type () == FileType.DIRECTORY) { 53 | create_album_folder (file.get_uri () + "/" + file_info.get_name ()); 54 | } 55 | } 56 | } catch (Error err) { 57 | warning (err.message); 58 | } 59 | } 60 | 61 | public MobilePhoneMusicFolder? get_sub_folder (string sub) { 62 | foreach (var child in this.children) { 63 | if ((child is MobilePhoneMusicFolder) && child.name == sub) { 64 | return child as MobilePhoneMusicFolder; 65 | } 66 | } 67 | 68 | var sub_folder = File.new_for_uri (file.get_uri () + "/" + sub); 69 | try { 70 | sub_folder.make_directory (); 71 | return create_album_folder (sub_folder.get_uri ()); 72 | } catch (Error err) { 73 | warning (err.message); 74 | } 75 | return null; 76 | } 77 | 78 | private MobilePhoneMusicFolder create_album_folder (string uri) { 79 | var sub = new MobilePhoneMusicFolder (uri); 80 | sub.subfolder_deleted.connect (() => subfolder_deleted ()); 81 | this.add (sub); 82 | return sub; 83 | } 84 | 85 | public void delete () { 86 | this.name = _("deleting…"); 87 | new Thread (null, () => { 88 | PlayMyMusic.Utils.delete_uri_recursive (file.get_uri ()); 89 | this.parent.remove (this); 90 | Idle.add (() => { 91 | subfolder_deleted (); 92 | return false; 93 | }); 94 | return null; 95 | }); 96 | } 97 | 98 | private bool data_drop_possible (Gdk.DragContext context, Gtk.SelectionData data) { 99 | var received = data.get_text (); 100 | 101 | if (!received.has_prefix ("Album:") && !received.has_prefix ("Artist:")) { 102 | return false; 103 | } 104 | 105 | var targets = PlayMyMusicApp.instance.mainwindow.mobile_phone_view.folders.root.children; 106 | foreach (var target in targets) { 107 | if (target == this) { 108 | return true; 109 | } 110 | } 111 | return false; 112 | } 113 | 114 | private Gdk.DragAction data_received (Gdk.DragContext context, Gtk.SelectionData data) { 115 | var received = data.get_text (); 116 | if (received.has_prefix ("Album:")) { 117 | var str_id = received.substring (6); 118 | 119 | int id = int.parse (str_id); 120 | if (id > 0) { 121 | var album = library_manager.get_album_by_id (id); 122 | if (album != null) { 123 | var current_mobile_phone = PlayMyMusicApp.instance.mainwindow.mobile_phone_view.current_mobile_phone; 124 | if (current_mobile_phone != null) { 125 | current_mobile_phone.add_album (album, this); 126 | } 127 | } 128 | } 129 | } else if (received.has_prefix ("Artist:")) { 130 | var str_id = received.substring (7); 131 | int id = int.parse (str_id); 132 | if (id > 0) { 133 | var artist = library_manager.get_artist_by_id (id); 134 | if (artist != null) { 135 | var current_mobile_phone = PlayMyMusicApp.instance.mainwindow.mobile_phone_view.current_mobile_phone; 136 | if (current_mobile_phone != null) { 137 | current_mobile_phone.add_artist (artist, this); 138 | } 139 | } 140 | } 141 | } 142 | 143 | return Gdk.DragAction.COPY; 144 | } 145 | 146 | public int compare (Granite.Widgets.SourceList.Item a, Granite.Widgets.SourceList.Item b) { 147 | if (a is MobilePhoneMusicFolder && b is MobilePhoneMusicFolder) { 148 | return (a as MobilePhoneMusicFolder).name.collate ((b as MobilePhoneMusicFolder).name); 149 | } 150 | return 0; 151 | } 152 | 153 | public bool allow_dnd_sorting () { 154 | return false; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Objects/Radio.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2017 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Objects { 29 | public class Radio : GLib.Object { 30 | PlayMyMusic.Services.LibraryManager library_manager; 31 | public signal void cover_changed (); 32 | public signal void removed (); 33 | 34 | int _ID = 0; 35 | public int ID { 36 | get { 37 | return _ID; 38 | } set { 39 | _ID = value; 40 | cover_path = GLib.Path.build_filename (PlayMyMusic.PlayMyMusicApp.instance.COVER_FOLDER, ("radio_%d.jpg").printf (this.ID)); 41 | load_cover (); 42 | } 43 | } 44 | public string title { get; set; default = ""; } 45 | public string url { get; set; default = ""; } 46 | 47 | public string cover_path { get; private set; default = ""; } 48 | 49 | string ? _file = null; 50 | public string ? file { 51 | get { 52 | if (_file == null) { 53 | _file = get_stream_file (); 54 | } 55 | 56 | return _file; 57 | } 58 | } 59 | 60 | Gdk.Pixbuf ? _cover = null; 61 | public Gdk.Pixbuf ? cover { 62 | get { 63 | return _cover; 64 | } set { 65 | _cover = value; 66 | cover_changed (); 67 | } 68 | } 69 | 70 | construct { 71 | library_manager = PlayMyMusic.Services.LibraryManager.instance; 72 | removed.connect ( 73 | () => { 74 | FileUtils.remove (cover_path); 75 | }); 76 | } 77 | 78 | public Radio () { 79 | } 80 | 81 | public void reset_stream_file () { 82 | _file = null; 83 | } 84 | 85 | private string ? get_stream_file () { 86 | string ? return_value = null; 87 | 88 | string ? content = get_stream_content (); 89 | if (content != null) { 90 | return_value = get_file_from_m3u (content); 91 | if (return_value == null) { 92 | return_value = get_file_from_pls (content); 93 | } 94 | } else { 95 | return_value = url; 96 | } 97 | 98 | return return_value; 99 | } 100 | 101 | private string ? get_stream_content () { 102 | string ? return_value = null; 103 | var session = new Soup.Session (); 104 | var msg = new Soup.Message ("GET", url); 105 | 106 | try { 107 | session.send (msg, null); 108 | } 109 | catch (Error err) { 110 | warning (err.message); 111 | return return_value; 112 | } 113 | var content_type = msg.response_headers.get_one ("Content-Type"); 114 | if (content_type != null && !content_type.has_prefix ("audio/mpeg") && !content_type.has_prefix ("audio/aac")) { 115 | session.send_message (msg); 116 | var data = (string)msg.response_body.data; 117 | if (msg.status_code == 200) { 118 | return_value = data; 119 | } 120 | } 121 | return return_value; 122 | } 123 | 124 | private string ? get_file_from_m3u (string content) { 125 | string[] lines = content.split ("\n"); 126 | foreach (unowned string line in lines) { 127 | if (line.has_prefix ("http") && line.index_of ("#") == -1) { 128 | return line; 129 | } 130 | } 131 | return null; 132 | } 133 | 134 | private string ? get_file_from_pls (string content) { 135 | string group = "playlist"; 136 | 137 | var file = new KeyFile (); 138 | try { 139 | file.load_from_data (content, -1, KeyFileFlags.NONE); 140 | } catch (Error err) { 141 | warning (err.message); 142 | } 143 | 144 | if (!file.has_group (group)) { 145 | return null; 146 | } 147 | 148 | try { 149 | foreach (unowned string key in file.get_keys (group)) { 150 | string val = file.get_value (group, key); 151 | if (key.down ().has_prefix ("file")) { 152 | return val; 153 | } 154 | } 155 | } catch (Error err) { 156 | warning (err.message); 157 | } 158 | 159 | return null; 160 | } 161 | 162 | public void set_new_cover (Gdk.Pixbuf cover) { 163 | this.cover = cover; 164 | save_cover (); 165 | } 166 | 167 | public void save_cover () { 168 | if (this.cover != null) { 169 | this.cover = library_manager.align_and_scale_pixbuf (this.cover, 64); 170 | try { 171 | this.cover.save (cover_path, "jpeg", "quality", "100"); 172 | } catch (Error err) { 173 | warning (err.message); 174 | } 175 | } 176 | } 177 | 178 | private void load_cover () { 179 | if (FileUtils.test (cover_path, FileTest.EXISTS)) { 180 | try { 181 | this.cover = new Gdk.Pixbuf.from_file (cover_path); 182 | } catch (Error err) { 183 | warning (err.message); 184 | } 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/Services/TagManager.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2017 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Services { 29 | public class TagManager : GLib.Object { 30 | static TagManager _instance = null; 31 | public static TagManager instance { 32 | get { 33 | if (_instance == null) { 34 | _instance = new TagManager (); 35 | } 36 | return _instance; 37 | } 38 | } 39 | 40 | public signal void discovered_new_item (PlayMyMusic.Objects.Artist artist, PlayMyMusic.Objects.Album album, PlayMyMusic.Objects.Track track); 41 | public signal void discover_started (); 42 | public signal void discover_finished (); 43 | 44 | Gst.PbUtils.Discoverer discoverer; 45 | 46 | public int discover_counter { get; private set; default = 0; } 47 | 48 | string unknown = _("Unknown"); 49 | 50 | construct { 51 | try { 52 | discoverer = new Gst.PbUtils.Discoverer ((Gst.ClockTime) (5 * Gst.SECOND)); 53 | discoverer.start (); 54 | discoverer.discovered.connect (discovered); 55 | } catch (Error err) { 56 | warning (err.message); 57 | } 58 | } 59 | 60 | private void discovered (Gst.PbUtils.DiscovererInfo info, Error? err) { 61 | new Thread (null, () => { 62 | string uri = info.get_uri (); 63 | if (info.get_result () != Gst.PbUtils.DiscovererResult.OK) { 64 | if (err != null) { 65 | warning ("DISCOVER ERROR: '%d' %s %s\n(%s)", err.code, err.message, info.get_result ().to_string (), uri); 66 | } 67 | } else { 68 | var tags = info.get_tags (); 69 | if (tags != null) { 70 | uint64 duration = info.get_duration (); 71 | string o; 72 | GLib.Date? d; 73 | Gst.DateTime? dt; 74 | uint u; 75 | uint64 u64 = 0; 76 | 77 | // TRACK REGION 78 | var track = new PlayMyMusic.Objects.Track (); 79 | track.duration = duration; 80 | track.uri = uri; 81 | if (tags.get_string (Gst.Tags.TITLE, out o)) { 82 | track.title = o; 83 | } 84 | if (track.title.strip () == "") { 85 | track.title = Path.get_basename (uri); 86 | } 87 | if (tags.get_uint (Gst.Tags.TRACK_NUMBER, out u)) { 88 | track.track = (int)u; 89 | } 90 | if (tags.get_uint (Gst.Tags.ALBUM_VOLUME_NUMBER, out u)) { 91 | track.disc = (int)u; 92 | } 93 | if (tags.get_string (Gst.Tags.GENRE, out o)) { 94 | track.genre = o; 95 | } 96 | if (track.duration == 0 && tags.get_uint64 (Gst.Tags.DURATION, out u64)) { 97 | track.duration = u64; 98 | } 99 | 100 | // ALBUM REGION 101 | var album = new PlayMyMusic.Objects.Album (); 102 | if (tags.get_string (Gst.Tags.ALBUM, out o)) { 103 | album.title = o; 104 | } 105 | 106 | if (album.title.strip () == "") { 107 | var dir = Path.get_dirname (uri); 108 | if (dir != null) { 109 | album.title = Path.get_basename (dir); 110 | } else { 111 | album.title = unknown; 112 | } 113 | } 114 | 115 | if (tags.get_date_time (Gst.Tags.DATE_TIME, out dt)) { 116 | if (dt != null) { 117 | album.year = dt.get_year (); 118 | } else if (tags.get_date (Gst.Tags.DATE, out d)) { 119 | if (d != null) { 120 | album.year = dt.get_year (); 121 | } 122 | } 123 | } 124 | 125 | // ARTIST REGION 126 | var artist = new PlayMyMusic.Objects.Artist (); 127 | if (tags.get_string (Gst.Tags.ALBUM_ARTIST, out o)) { 128 | artist.name = o; 129 | } else if (tags.get_string (Gst.Tags.ARTIST, out o)) { 130 | artist.name = o; 131 | } 132 | 133 | if (artist.name.strip () == "") { 134 | var dir = Path.get_dirname (Path.get_dirname (uri)); 135 | if (dir != null) { 136 | artist.name = Path.get_basename (dir); 137 | } else { 138 | artist.name = unknown; 139 | } 140 | } 141 | 142 | discovered_new_item (artist, album, track); 143 | } 144 | } 145 | 146 | discover_counter--; 147 | if (discover_counter == 0) { 148 | discover_finished (); 149 | } 150 | 151 | info.dispose (); 152 | return null; 153 | }); 154 | } 155 | 156 | public void add_discover_uri (string uri) { 157 | if (discover_counter == 0) { 158 | discover_started (); 159 | } 160 | discover_counter++; 161 | discoverer.discover_uri_async (uri); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Widgets/Views/MobilePhone.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Widgets.Views { 29 | public class MobilePhone : Gtk.Revealer { 30 | Services.LibraryManager library_manager; 31 | 32 | public Objects.MobilePhone ? current_mobile_phone { get; private set; default = null; } 33 | public Granite.Widgets.SourceList folders { get; private set; } 34 | 35 | Gtk.Label title; 36 | Gtk.Image image; 37 | Gtk.ProgressBar progress; 38 | Gtk.Label message; 39 | Gtk.Spinner spinner; 40 | public bool stay_closed { get; private set; default = false; } 41 | 42 | construct { 43 | library_manager = Services.LibraryManager.instance; 44 | library_manager.mobile_phone_connected.connect ( 45 | (mobile_phone) => { 46 | show_mobile_phone (mobile_phone); 47 | this.reveal_child = true; 48 | }); 49 | library_manager.mobile_phone_disconnected.connect ( 50 | (volume) => { 51 | if (current_mobile_phone.volume == volume) { 52 | this.reveal_child = false; 53 | reset (); 54 | } 55 | }); 56 | } 57 | 58 | public MobilePhone () { 59 | build_ui (); 60 | 61 | const Gtk.TargetEntry[] targetentries = {{ "STRING", 0, 0 }}; 62 | 63 | folders.enable_drag_dest (targetentries, Gdk.DragAction.COPY); 64 | } 65 | 66 | private void build_ui () { 67 | var header = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); 68 | header.margin = 12; 69 | header.spacing = 12; 70 | 71 | title = new Gtk.Label (""); 72 | title.ellipsize = Pango.EllipsizeMode.END; 73 | 74 | image = new Gtk.Image (); 75 | 76 | spinner = new Gtk.Spinner (); 77 | spinner.height_request = 48; 78 | 79 | progress = new Gtk.ProgressBar (); 80 | 81 | message = new Gtk.Label (_("Enable MTP on your mobile phone")); 82 | message.wrap = true; 83 | message.max_width_chars = 0; 84 | message.justify = Gtk.Justification.CENTER; 85 | message.margin_top = 12; 86 | message.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); 87 | 88 | header.pack_start (title, false, false, 0); 89 | header.pack_start (image); 90 | header.pack_start (spinner); 91 | header.pack_start (progress); 92 | header.pack_start (message); 93 | 94 | folders = new Granite.Widgets.SourceList (); 95 | folders.hexpand = false; 96 | folders.events |= Gdk.EventMask.KEY_RELEASE_MASK; 97 | folders.key_release_event.connect ( 98 | (key) => { 99 | if (key.keyval == Gdk.Key.Delete && (folders.selected is Objects.MobilePhoneMusicFolder)) { 100 | (folders.selected as Objects.MobilePhoneMusicFolder).delete (); 101 | } 102 | return true; 103 | }); 104 | 105 | var close_button = new Gtk.Button.from_icon_name ("pane-hide-symbolic-rtl", Gtk.IconSize.BUTTON); 106 | close_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); 107 | close_button.get_style_context ().add_class ("mobile-close-button"); 108 | close_button.tooltip_text = _("Hide Mobile Panel"); 109 | close_button.valign = Gtk.Align.START; 110 | close_button.halign = Gtk.Align.END; 111 | close_button.clicked.connect ( 112 | () => { 113 | this.reveal_child = false; 114 | stay_closed = true; 115 | }); 116 | 117 | var content = new Gtk.Grid (); 118 | content.attach (close_button, 0, 0); 119 | content.attach (header, 0, 0); 120 | content.attach (folders, 0, 1); 121 | content.attach (new Gtk.Separator (Gtk.Orientation.VERTICAL), 1, 0, 1, 2); 122 | 123 | this.transition_type = Gtk.RevealerTransitionType.SLIDE_RIGHT; 124 | 125 | this.add (content); 126 | this.show_all (); 127 | } 128 | 129 | public void hide_spinner () { 130 | spinner.hide (); 131 | } 132 | 133 | public void show_mobile_phone (Objects.MobilePhone mobile_phone) { 134 | if (current_mobile_phone == mobile_phone) { 135 | return; 136 | } 137 | 138 | if (current_mobile_phone != null) { 139 | current_mobile_phone.storage_calculated.disconnect (storage_calculated); 140 | current_mobile_phone.music_folder_found.disconnect (music_folder_found); 141 | current_mobile_phone.copy_progress.disconnect (copy_progress); 142 | current_mobile_phone.copy_finished.disconnect (copy_finished); 143 | current_mobile_phone.copy_started.disconnect (copy_started); 144 | } 145 | 146 | reset (); 147 | 148 | current_mobile_phone = mobile_phone; 149 | 150 | title.label = current_mobile_phone.volume.get_name (); 151 | image.set_from_gicon (current_mobile_phone.volume.get_icon (), Gtk.IconSize.DIALOG); 152 | 153 | current_mobile_phone.storage_calculated.connect (storage_calculated); 154 | current_mobile_phone.music_folder_found.connect (music_folder_found); 155 | current_mobile_phone.copy_progress.connect (copy_progress); 156 | current_mobile_phone.copy_finished.connect (copy_finished); 157 | current_mobile_phone.copy_started.connect (copy_started); 158 | } 159 | 160 | public void reset () { 161 | current_mobile_phone = null; 162 | stay_closed = false; 163 | folders.root.clear (); 164 | message.show (); 165 | } 166 | 167 | private void storage_calculated () { 168 | progress.fraction = 1 - (double)1 / current_mobile_phone.size * current_mobile_phone.free; 169 | } 170 | 171 | private void copy_progress (string title, uint count, uint sum) { 172 | progress.fraction = (double)1 / sum * count; 173 | } 174 | 175 | private void copy_finished () { 176 | image.show (); 177 | spinner.hide (); 178 | spinner.active = false; 179 | storage_calculated (); 180 | } 181 | 182 | private void copy_started () { 183 | image.hide (); 184 | spinner.show (); 185 | spinner.active = true; 186 | } 187 | 188 | private void music_folder_found (Objects.MobilePhoneMusicFolder music_folder) { 189 | message.hide (); 190 | music_folder.collapsible = false; 191 | 192 | Idle.add ( 193 | () => { 194 | folders.root.add (music_folder); 195 | return false; 196 | }); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Interfaces/SoundIndicator.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Interfaces { 29 | public class SoundIndicator { 30 | public static SoundIndicator instance { get; private set; } 31 | public static void listen () { 32 | instance = new SoundIndicator (); 33 | instance.initialize (); 34 | } 35 | 36 | SoundIndicatorPlayer player; 37 | SoundIndicatorRoot root; 38 | 39 | unowned DBusConnection conn; 40 | uint owner_id; 41 | uint root_id; 42 | uint player_id; 43 | 44 | private void initialize () { 45 | owner_id = Bus.own_name (BusType.SESSION, "org.mpris.MediaPlayer2.PlayMyMusic", GLib.BusNameOwnerFlags.NONE, on_bus_acquired, on_name_acquired, on_name_lost); 46 | if (owner_id == 0) { 47 | warning ("Could not initialize MPRIS session.\n"); 48 | } 49 | PlayMyMusic.PlayMyMusicApp.instance.mainwindow.destroy.connect (() => { 50 | this.conn.unregister_object (root_id); 51 | this.conn.unregister_object (player_id); 52 | Bus.unown_name (owner_id); 53 | }); 54 | } 55 | 56 | private void on_bus_acquired (DBusConnection connection, string name) { 57 | this.conn = connection; 58 | try { 59 | root = new SoundIndicatorRoot (); 60 | root_id = connection.register_object ("/org/mpris/MediaPlayer2", root); 61 | player = new SoundIndicatorPlayer (connection); 62 | player_id = connection.register_object ("/org/mpris/MediaPlayer2", player); 63 | } 64 | catch(Error e) { 65 | warning ("could not create MPRIS player: %s\n", e.message); 66 | } 67 | } 68 | 69 | private void on_name_acquired (DBusConnection connection, string name) { 70 | } 71 | 72 | private void on_name_lost (DBusConnection connection, string name) { 73 | } 74 | } 75 | 76 | [DBus(name = "org.mpris.MediaPlayer2")] 77 | public class SoundIndicatorRoot : GLib.Object { 78 | PlayMyMusicApp app; 79 | 80 | construct { 81 | this.app = PlayMyMusicApp.instance; 82 | } 83 | 84 | public string DesktopEntry { 85 | owned get { 86 | return app.application_id; 87 | } 88 | } 89 | } 90 | 91 | [DBus(name = "org.mpris.MediaPlayer2.Player")] 92 | public class SoundIndicatorPlayer : GLib.Object { 93 | Services.Player player; 94 | DBusConnection connection; 95 | PlayMyMusicApp app; 96 | 97 | public SoundIndicatorPlayer (DBusConnection connection) { 98 | this.app = PlayMyMusicApp.instance; 99 | this.connection = connection; 100 | player = Services.Player.instance; 101 | player.state_changed.connect_after (player_state_changed); 102 | } 103 | 104 | private static string[] get_simple_string_array (string text) { 105 | string[] array = new string[0]; 106 | array += text; 107 | return array; 108 | } 109 | 110 | private void send_properties (string property, Variant val) { 111 | var property_list = new HashTable (str_hash, str_equal); 112 | property_list.insert (property, val); 113 | 114 | var builder = new VariantBuilder (VariantType.ARRAY); 115 | var invalidated_builder = new VariantBuilder (new VariantType("as")); 116 | 117 | foreach(string name in property_list.get_keys ()) { 118 | Variant variant = property_list.lookup (name); 119 | builder.add ("{sv}", name, variant); 120 | } 121 | 122 | try { 123 | connection.emit_signal (null, 124 | "/org/mpris/MediaPlayer2", 125 | "org.freedesktop.DBus.Properties", 126 | "PropertiesChanged", 127 | new Variant("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", builder, invalidated_builder)); 128 | } 129 | catch(Error e) { 130 | print("Could not send MPRIS property change: %s\n", e.message); 131 | } 132 | } 133 | 134 | public bool CanGoNext { get { return true; } } 135 | 136 | public bool CanGoPrevious { get { return true; } } 137 | 138 | public bool CanPlay { get { return true; } } 139 | 140 | public bool CanPause { get { return true; } } 141 | 142 | public void PlayPause () throws Error { 143 | app.mainwindow.toggle_playing (); 144 | } 145 | 146 | public void Next () throws Error { 147 | app.mainwindow.next (); 148 | } 149 | 150 | public void Previous() throws Error { 151 | app.mainwindow.prev (); 152 | } 153 | 154 | private void player_state_changed (Gst.State state) { 155 | Variant property; 156 | switch (state) { 157 | case Gst.State.PLAYING: 158 | property = "Playing"; 159 | var metadata = new HashTable (null, null); 160 | if (player.current_track != null) { 161 | if (player.play_mode == Services.PlayMode.AUDIO_CD) { 162 | metadata.insert("xesam:title", player.current_track.title); 163 | metadata.insert("xesam:artist", get_simple_string_array (player.current_track.audio_cd.artist)); 164 | } else { 165 | var file = File.new_for_path (player.current_track.album.cover_path); 166 | metadata.insert("mpris:artUrl", file.get_uri ()); 167 | metadata.insert("xesam:title", player.current_track.title); 168 | metadata.insert("xesam:artist", get_simple_string_array (player.current_track.album.artist.name)); 169 | } 170 | } else if (player.current_radio != null) { 171 | var file = File.new_for_path (player.current_radio.cover_path); 172 | metadata.insert("mpris:artUrl", file.get_uri ()); 173 | metadata.insert("xesam:title", player.current_radio.title); 174 | metadata.insert("xesam:artist", get_simple_string_array (player.current_radio.url)); 175 | } 176 | send_properties ("Metadata", metadata); 177 | break; 178 | case Gst.State.PAUSED: 179 | property = "Paused"; 180 | break; 181 | default: 182 | property = "Stopped"; 183 | var metadata = new HashTable (null, null); 184 | metadata.insert("mpris:artUrl", ""); 185 | metadata.insert("xesam:title", ""); 186 | metadata.insert("xesam:artist", new string [0]); 187 | send_properties ("Metadata", metadata); 188 | break; 189 | } 190 | send_properties ("PlaybackStatus", property); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Widgets/Views/AlbumView.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Widgets.Views { 29 | public class AlbumView : Gtk.Grid { 30 | Services.LibraryManager library_manager; 31 | Services.Player player; 32 | Settings settings; 33 | 34 | Gtk.Menu menu; 35 | Gtk.Image cover; 36 | Gtk.ListBox tracks; 37 | 38 | bool only_mark = false; 39 | 40 | public Objects.Album current_album { get; private set; } 41 | 42 | construct { 43 | settings = Settings.get_default (); 44 | library_manager = Services.LibraryManager.instance; 45 | player = library_manager.player; 46 | player.state_changed.connect ((state) => { 47 | mark_playing_track (player.current_track); 48 | }); 49 | } 50 | 51 | public AlbumView () { 52 | build_ui (); 53 | } 54 | 55 | private void build_ui () { 56 | var content = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); 57 | content.vexpand = true; 58 | var event_box = new Gtk.EventBox (); 59 | event_box.button_press_event.connect (show_context_menu); 60 | 61 | cover = new Gtk.Image (); 62 | event_box.add (cover); 63 | 64 | menu = new Gtk.Menu (); 65 | var menu_new_cover = new Gtk.MenuItem.with_label (_("Set new Cover…")); 66 | menu_new_cover.activate.connect (() => { 67 | var new_cover = library_manager.choose_new_cover (); 68 | if (new_cover != null) { 69 | try { 70 | var pixbuf = new Gdk.Pixbuf.from_file (new_cover); 71 | current_album.set_new_cover (pixbuf, 256); 72 | if (settings.save_custom_covers) { 73 | current_album.set_custom_cover_file (new_cover); 74 | } 75 | } catch (Error err) { 76 | warning (err.message); 77 | } 78 | } 79 | }); 80 | menu.append (menu_new_cover); 81 | menu.show_all (); 82 | 83 | var tracks_scroll = new Gtk.ScrolledWindow (null, null); 84 | 85 | tracks = new Gtk.ListBox (); 86 | tracks.set_sort_func (tracks_sort_func); 87 | tracks.selected_rows_changed.connect (play_track); 88 | tracks_scroll.add (tracks); 89 | 90 | content.pack_start (event_box, false, false, 0); 91 | content.pack_start (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), false, false, 0); 92 | content.pack_start (tracks_scroll, true, true, 0); 93 | 94 | this.attach (new Gtk.Separator (Gtk.Orientation.VERTICAL), 0, 0); 95 | this.attach (content, 1, 0); 96 | } 97 | 98 | public void mark_playing_track (Objects.Track? track) { 99 | tracks.unselect_all (); 100 | if (track == null) { 101 | return; 102 | } 103 | foreach (var child in tracks.get_children ()) { 104 | if ((child as Widgets.Track).track.ID == track.ID) { 105 | only_mark = true; 106 | child.activate (); 107 | if (PlayMyMusicApp.instance.mainwindow.content.visible_child_name == "albums") { 108 | child.grab_focus (); 109 | } 110 | only_mark = false; 111 | return; 112 | } 113 | } 114 | } 115 | 116 | public void play_album () { 117 | Objects.Track? track; 118 | if (settings.shuffle_mode) { 119 | track = current_album.get_shuffle_track (null); 120 | } else { 121 | track = current_album.get_first_track (); 122 | } 123 | library_manager.play_track (track, Services.PlayMode.ALBUM); 124 | } 125 | 126 | private void play_track () { 127 | var selected_row = tracks.get_selected_row (); 128 | if (selected_row != null && !only_mark) { 129 | library_manager.play_track ((selected_row as Widgets.Track).track, Services.PlayMode.ALBUM); 130 | } 131 | } 132 | 133 | public void show_album (Objects.Album album) { 134 | if (current_album == album) { 135 | return; 136 | } 137 | 138 | if (current_album != null) { 139 | current_album.track_added.disconnect (add_track); 140 | current_album.cover_changed.disconnect (change_cover); 141 | } 142 | current_album = album; 143 | 144 | reset (); 145 | if (current_album.cover == null) { 146 | cover.set_from_icon_name ("audio-x-generic-symbolic", Gtk.IconSize.DIALOG); 147 | cover.height_request = 256; 148 | cover.width_request = 256; 149 | } else { 150 | cover.pixbuf = current_album.cover; 151 | } 152 | 153 | cover.tooltip_markup = ("%s%s\n%s").printf (album.title.replace ("&", "&"), album.year > 0 ? (" (%d)").printf (album.year) : "", album.artist.name.replace ("&", "&")); 154 | 155 | foreach (var track in current_album.tracks) { 156 | add_track (track); 157 | } 158 | current_album.track_added.connect (add_track); 159 | current_album.cover_changed.connect (change_cover); 160 | } 161 | 162 | private void reset () { 163 | foreach (var child in tracks.get_children ()) { 164 | child.destroy (); 165 | } 166 | } 167 | 168 | private void change_cover () { 169 | cover.pixbuf = current_album.cover; 170 | } 171 | 172 | private void add_track (Objects.Track track) { 173 | if (track.file_exists ()) { 174 | Idle.add (() => { 175 | var item = new Widgets.Track (track); 176 | tracks.add (item); 177 | if (player.current_track != null && player.current_track.ID == track.ID) { 178 | item.activate (); 179 | } 180 | return false; 181 | }); 182 | } 183 | } 184 | 185 | private bool show_context_menu (Gtk.Widget sender, Gdk.EventButton evt) { 186 | if (evt.type == Gdk.EventType.BUTTON_PRESS && evt.button == 3) { 187 | menu.popup_at_pointer (null); 188 | return true; 189 | } 190 | return false; 191 | } 192 | 193 | private int tracks_sort_func (Gtk.ListBoxRow child1, Gtk.ListBoxRow child2) { 194 | var item1 = (Widgets.Track)child1; 195 | var item2 = (Widgets.Track)child2; 196 | if (item1 != null && item2 != null) { 197 | if (item1.disc_number != item2.disc_number){ 198 | return item1.disc_number - item2.disc_number; 199 | } 200 | if (item1.track_number != item2.track_number){ 201 | return item1.track_number - item2.track_number; 202 | } 203 | return item1.title.collate (item2.title); 204 | } 205 | return 0; 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Objects/AudioCD.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Objects { 29 | public class AudioCD : TracksContainer { 30 | const string FILE_ATTRIBUTE_TITLE = "xattr::org.gnome.audio.title"; 31 | const string FILE_ATTRIBUTE_ARTIST = "xattr::org.gnome.audio.artist"; 32 | const string FILE_ATTRIBUTE_DURATION = "xattr::org.gnome.audio.duration"; 33 | 34 | public signal void mb_disc_id_calculated (); 35 | 36 | public Volume volume { get; private set; } 37 | public string artist { get; set; } 38 | 39 | string _mb_disc_id = ""; 40 | public string mb_disc_id { 41 | get { 42 | return _mb_disc_id; 43 | } private set { 44 | _mb_disc_id = value; 45 | this.cover_path = GLib.Path.build_filename (PlayMyMusicApp.instance.COVER_FOLDER, ("audio_cd_%s.jpg").printf (this.mb_disc_id)); 46 | this.background_path = GLib.Path.build_filename (PlayMyMusicApp.instance.COVER_FOLDER, ("audio_cd_%s_background.png").printf (this.mb_disc_id)); 47 | load_cover_async.begin (); 48 | mb_disc_id_calculated (); 49 | } 50 | } 51 | 52 | public new GLib.List tracks { 53 | get { 54 | return _tracks; 55 | } 56 | } 57 | 58 | construct { 59 | this.cover_changed.connect (() => { 60 | ID = -1; 61 | create_background (); 62 | }); 63 | } 64 | 65 | public AudioCD (Volume volume) { 66 | this.volume = volume; 67 | this.title = _ ("Unknown"); 68 | this.artist = _ ("Unknown"); 69 | this.volume.mount.begin (MountMountFlags.NONE, null, null, (obj, res)=>{ 70 | create_track_list (); 71 | calculate_disc_id (); 72 | }); 73 | } 74 | 75 | public void create_track_list () { 76 | var file = this.volume.get_activation_root (); 77 | var attributes = new string[0]; 78 | attributes += FILE_ATTRIBUTE_TITLE; 79 | attributes += FILE_ATTRIBUTE_DURATION; 80 | attributes += FILE_ATTRIBUTE_ARTIST; 81 | attributes += FileAttribute.STANDARD_NAME; 82 | 83 | file.query_info_async.begin ( 84 | string.joinv (",", attributes), 85 | FileQueryInfoFlags.NONE, 86 | Priority.DEFAULT, 87 | null, 88 | (obj, res) => { 89 | try { 90 | FileInfo file_info = file.query_info_async.end (res); 91 | string ? album_title = file_info.get_attribute_string (FILE_ATTRIBUTE_TITLE); 92 | if (album_title != null) { 93 | this.title = album_title; 94 | } 95 | string ? album_artist = file_info.get_attribute_string (FILE_ATTRIBUTE_ARTIST); 96 | if (album_artist != null) { 97 | this.artist = album_artist.strip (); 98 | } 99 | 100 | int counter = 1; 101 | var children = file.enumerate_children (string.joinv (",", attributes), GLib.FileQueryInfoFlags.NONE); 102 | while ((file_info = children.next_file ()) != null) { 103 | string ? title = file_info.get_attribute_string (FILE_ATTRIBUTE_TITLE); 104 | if (title == null) { 105 | title = _ ("Track %d").printf (counter); 106 | } 107 | uint64 duration = file_info.get_attribute_uint64 (FILE_ATTRIBUTE_DURATION); 108 | 109 | var track = new Track (this); 110 | track.ID = counter * -1; 111 | track.track = counter; 112 | track.title = title.strip (); 113 | track.uri = GLib.Path.build_filename (file.get_uri (), file_info.get_name ()); 114 | track.duration = duration * Gst.SECOND; 115 | add_track (track); 116 | counter++; 117 | } 118 | } catch (Error err) { 119 | warning (err.message); 120 | } 121 | }); 122 | } 123 | 124 | public void update_track_title (int num, string title) { 125 | foreach (var track in tracks) { 126 | if (track.track == num) { 127 | track.title = title; 128 | } 129 | } 130 | } 131 | 132 | private void calculate_disc_id () { 133 | new Thread ( 134 | "calculate_disc_id", 135 | () => { 136 | dynamic Gst.Element source = null; 137 | try { 138 | source = Gst.Element.make_from_uri (Gst.URIType.SRC, "cdda://", null); 139 | } catch (Error err) { 140 | warning (err.message); 141 | } 142 | if (source == null) { 143 | return null; 144 | } 145 | source.@set ("device", "/dev/cdrom", null); 146 | dynamic Gst.Element pipeline = new Gst.Pipeline (null); 147 | dynamic Gst.Element sink = Gst.ElementFactory.make ("fakesink", null); 148 | (pipeline as Gst.Bin).add_many (source, sink); 149 | source.link (sink); 150 | pipeline.set_state (Gst.State.PAUSED); 151 | Gst.Bus bus = pipeline.get_bus (); 152 | bool done = false; 153 | while (!done) { 154 | Gst.Message ? msg; 155 | Gst.TagList tags; 156 | GLib.Error err; 157 | msg = bus.timed_pop (10 * Gst.SECOND); 158 | if (msg == null) { 159 | break; 160 | } 161 | switch (msg.type) { 162 | case Gst.MessageType.TAG : 163 | msg.parse_tag (out tags); 164 | string s; 165 | if (tags.get_string (Gst.Tag.CDDA.MUSICBRAINZ_DISCID, out s)) { 166 | mb_disc_id = s; 167 | stdout.printf ("MB DISC ID: %s\n", mb_disc_id); 168 | } 169 | done = true; 170 | break; 171 | case Gst.MessageType.ERROR : 172 | string debug; 173 | msg.parse_error (out err, out debug); 174 | warning ("Error: %s\n%s\n", err.message, debug); 175 | done = true; 176 | break; 177 | } 178 | } 179 | pipeline.set_state (Gst.State.NULL); 180 | return null; 181 | }); 182 | } 183 | 184 | public async void load_cover_async () { 185 | if (FileUtils.test (cover_path, FileTest.EXISTS)) { 186 | try { 187 | cover = new Gdk.Pixbuf.from_file (cover_path); 188 | } catch (Error err) { 189 | warning (err.message); 190 | } 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Widgets/Views/ArtistsView.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Widgets.Views { 29 | public class ArtistsView : Gtk.Grid { 30 | Services.LibraryManager library_manager; 31 | Settings settings; 32 | MainWindow mainwindow; 33 | 34 | private string _filter = ""; 35 | public string filter { 36 | get { 37 | return _filter; 38 | } set { 39 | if (_filter != value) { 40 | _filter = value; 41 | do_filter (); 42 | } 43 | } 44 | } 45 | 46 | Gtk.FlowBox artists; 47 | Gtk.Stack stack; 48 | 49 | Widgets.Views.ArtistView artist_view; 50 | 51 | uint timer_sort = 0; 52 | uint items_found = 0; 53 | 54 | construct { 55 | settings = Settings.get_default (); 56 | library_manager = Services.LibraryManager.instance; 57 | library_manager.added_new_artist.connect ((artist) => { 58 | Idle.add (() => { 59 | add_artist (artist); 60 | return false; 61 | }); 62 | }); 63 | } 64 | 65 | public signal void artist_selected (); 66 | 67 | public ArtistsView (MainWindow mainwindow) { 68 | this.mainwindow = mainwindow; 69 | this.mainwindow.ctrl_press.connect (() => { 70 | foreach (var child in artists.get_selected_children ()) { 71 | var artist = child as Widgets.Artist; 72 | if (!artist.multi_selection) { 73 | artist.toggle_multi_selection (false); 74 | } 75 | } 76 | }); 77 | build_ui (); 78 | this.draw.connect (first_draw); 79 | } 80 | 81 | private bool first_draw () { 82 | this.draw.disconnect (first_draw); 83 | activate_by_id (settings.last_artist_id); 84 | load_background (); 85 | return false; 86 | } 87 | 88 | private void build_ui () { 89 | artists = new Gtk.FlowBox (); 90 | artists.margin = 24; 91 | artists.row_spacing = 12; 92 | artists.valign = Gtk.Align.START; 93 | artists.max_children_per_line = 1; 94 | artists.selection_mode = Gtk.SelectionMode.MULTIPLE; 95 | artists.set_filter_func (artists_filter_func); 96 | artists.child_activated.connect (show_artist_viewer); 97 | 98 | var artists_scroll = new Gtk.ScrolledWindow (null, null); 99 | artists_scroll.width_request = 200; 100 | artists_scroll.add (artists); 101 | 102 | artist_view = new PlayMyMusic.Widgets.Views.ArtistView (); 103 | artist_view.expand = true; 104 | 105 | var content = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); 106 | content.expand = true; 107 | content.pack_start (artists_scroll, false, false, 0); 108 | content.pack_end (artist_view, true, true, 0); 109 | 110 | var alert_view = new Granite.Widgets.AlertView (_("No results"), _("Try another search"), "edit-find-symbolic"); 111 | 112 | stack = new Gtk.Stack (); 113 | stack.add_named (content, "content"); 114 | stack.add_named (alert_view, "alert"); 115 | 116 | this.add (stack); 117 | this.show_all (); 118 | } 119 | 120 | public void add_artist (Objects.Artist artist) { 121 | var a = new Widgets.Artist (artist); 122 | lock (artists) { 123 | artists.add (a); 124 | } 125 | a.merge.connect (() => { 126 | GLib.List selected = new GLib.List (); 127 | foreach (var child in artists.get_selected_children ()){ 128 | selected.append ((child as Widgets.Artist).artist); 129 | } 130 | artist.merge (selected); 131 | }); 132 | do_sort (); 133 | } 134 | 135 | private void do_sort () { 136 | lock (timer_sort) { 137 | if (timer_sort != 0) { 138 | Source.remove (timer_sort); 139 | timer_sort = 0; 140 | } 141 | 142 | timer_sort = Timeout.add (500, () => { 143 | artists.set_sort_func (artists_sort_func); 144 | artists.set_sort_func (null); 145 | Source.remove (timer_sort); 146 | timer_sort = 0; 147 | return false; 148 | }); 149 | } 150 | } 151 | 152 | private void do_filter () { 153 | items_found = 0; 154 | artists.invalidate_filter (); 155 | if (items_found == 0) { 156 | stack.visible_child_name = "alert"; 157 | } else { 158 | stack.visible_child_name = "content"; 159 | } 160 | } 161 | 162 | public void activate_by_track (Objects.Track track) { 163 | activate_by_id (track.album.artist.ID); 164 | } 165 | 166 | public Objects.Artist? activate_by_id (int id) { 167 | foreach (var child in artists.get_children ()) { 168 | if ((child as Widgets.Artist).artist.ID == id) { 169 | child.activate (); 170 | return (child as Widgets.Artist).artist; 171 | } 172 | } 173 | return null; 174 | } 175 | 176 | public void reset () { 177 | filter = ""; 178 | foreach (var child in artists.get_children ()) { 179 | child.destroy (); 180 | } 181 | artist_view.reset (); 182 | } 183 | 184 | public void play_selected_artist () { 185 | if (artist_view.current_artist != null) { 186 | artist_view.play_artist (); 187 | } 188 | } 189 | 190 | public void load_background () { 191 | artist_view.load_background (); 192 | } 193 | 194 | private void show_artist_viewer (Gtk.FlowBoxChild item) { 195 | if (mainwindow.ctrl_pressed) { 196 | if ((item as Widgets.Artist).multi_selection) { 197 | artists.unselect_child (item); 198 | (item as Widgets.Artist).reset (); 199 | return; 200 | } else { 201 | (item as Widgets.Artist).toggle_multi_selection (false); 202 | } 203 | } 204 | if (!(item as Widgets.Artist).multi_selection) { 205 | foreach (var child in artists.get_selected_children ()) { 206 | (child as Widgets.Artist).reset (); 207 | } 208 | artists.unselect_all (); 209 | artists.select_child (item); 210 | } 211 | var artist = (item as Widgets.Artist).artist; 212 | settings.last_artist_id = artist.ID; 213 | artist_view.show_artist_viewer (artist); 214 | artist_selected (); 215 | } 216 | 217 | private bool artists_filter_func (Gtk.FlowBoxChild child) { 218 | if (filter.strip ().length == 0) { 219 | items_found ++; 220 | return true; 221 | } 222 | 223 | string[] filter_elements = filter.strip ().down ().split (" "); 224 | var artist = (child as Widgets.Artist).artist; 225 | 226 | foreach (string filter_element in filter_elements) { 227 | if (!artist.name.down ().contains (filter_element)) { 228 | bool track_title = false; 229 | foreach (var track in artist.tracks) { 230 | if (track.title.down ().contains (filter_element) || track.genre.down ().contains (filter_element) || track.album.title.down ().contains (filter_element)) { 231 | track_title = true; 232 | } 233 | } 234 | if (track_title) { 235 | continue; 236 | } 237 | return false; 238 | } 239 | } 240 | items_found ++; 241 | return true; 242 | } 243 | 244 | private int artists_sort_func (Gtk.FlowBoxChild child1, Gtk.FlowBoxChild child2) { 245 | var item1 = (Widgets.Artist)child1; 246 | var item2 = (Widgets.Artist)child2; 247 | if (item1 != null && item2 != null) { 248 | return item1.name.collate (item2.name); 249 | } 250 | return 0; 251 | } 252 | 253 | public void unselect_all () { 254 | foreach (var child in artists.get_selected_children ()) { 255 | (child as Widgets.Artist).reset (); 256 | } 257 | artists.unselect_all (); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/Widgets/Views/ArtistView.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Widgets.Views { 29 | public class ArtistView : Gtk.Grid { 30 | Services.LibraryManager library_manager; 31 | Services.Player player; 32 | Settings settings; 33 | 34 | Gtk.ListBox tracks; 35 | 36 | Gtk.Box content; 37 | 38 | Gtk.Label artist_name; 39 | Gtk.Label artist_sub_title; 40 | Gtk.Image background; 41 | Gtk.Box header; 42 | Granite.Widgets.AlertView alert_view; 43 | 44 | bool only_mark = false; 45 | 46 | public Objects.Artist current_artist { get; private set; } 47 | 48 | construct { 49 | settings = Settings.get_default (); 50 | library_manager = Services.LibraryManager.instance; 51 | player = Services.Player.instance; 52 | player.state_changed.connect ((state) => { 53 | mark_playing_track (player.current_track); 54 | }); 55 | 56 | settings.notify["use-dark-theme"].connect (() => { 57 | if (settings.use_dark_theme) { 58 | tracks.get_style_context ().add_class ("artist-tracks-dark"); 59 | tracks.get_style_context ().remove_class ("artist-tracks"); 60 | } else { 61 | tracks.get_style_context ().add_class ("artist-tracks"); 62 | tracks.get_style_context ().remove_class ("artist-tracks-dark"); 63 | } 64 | }); 65 | } 66 | 67 | public ArtistView () { 68 | build_ui (); 69 | } 70 | 71 | private void build_ui () { 72 | content = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); 73 | content.expand = true; 74 | 75 | header = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); 76 | header.height_request = 256; 77 | header.valign = Gtk.Align.CENTER; 78 | 79 | artist_name = new Gtk.Label (""); 80 | artist_name.valign = Gtk.Align.END; 81 | artist_name.get_style_context ().add_class (Granite.STYLE_CLASS_H1_LABEL); 82 | header.pack_start (artist_name, true, true); 83 | 84 | artist_sub_title = new Gtk.Label (""); 85 | artist_sub_title.valign = Gtk.Align.START; 86 | artist_sub_title.use_markup = true; 87 | artist_sub_title.opacity = 0.75; 88 | header.pack_start (artist_sub_title, true, true); 89 | 90 | background = new Gtk.Image (); 91 | background.expand = true; 92 | 93 | var tracks_scroll = new Gtk.ScrolledWindow (null, null); 94 | tracks_scroll.expand = true; 95 | 96 | tracks = new Gtk.ListBox (); 97 | tracks.set_sort_func (tracks_sort_func); 98 | tracks.selected_rows_changed.connect (play_track); 99 | if (settings.use_dark_theme) { 100 | tracks.get_style_context ().add_class ("artist-tracks-dark"); 101 | } else { 102 | tracks.get_style_context ().add_class ("artist-tracks"); 103 | } 104 | tracks_scroll.add (tracks); 105 | 106 | content.pack_start (header, false, false, 0); 107 | content.pack_start (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), false, false, 0); 108 | content.pack_start (tracks_scroll, true, true, 0); 109 | 110 | alert_view = new Granite.Widgets.AlertView (_("Choose an Artist"), _("No Artist selected"), "avatar-default-symbolic"); 111 | 112 | var overlay = new Gtk.Overlay (); 113 | overlay.add_overlay (background); 114 | overlay.add_overlay (content); 115 | overlay.add_overlay (alert_view); 116 | 117 | this.attach (new Gtk.Separator (Gtk.Orientation.VERTICAL), 0, 0); 118 | this.attach (overlay, 1, 0); 119 | } 120 | 121 | public void show_artist_viewer (Objects.Artist artist) { 122 | if (current_artist == artist) { 123 | return; 124 | } 125 | 126 | if (current_artist != null) { 127 | current_artist.track_added.disconnect (add_track); 128 | current_artist.background_changed.disconnect (load_background); 129 | } 130 | current_artist = artist; 131 | this.reset (); 132 | alert_view.hide (); 133 | 134 | load_background (); 135 | foreach (var track in artist.tracks) { 136 | add_track (track); 137 | } 138 | 139 | current_artist.track_added.connect (add_track); 140 | current_artist.background_changed.connect (load_background); 141 | } 142 | 143 | public void load_background () { 144 | int width = this.content.get_allocated_width (); 145 | int height = this.content.get_allocated_height (); 146 | if (current_artist == null || current_artist.background_path == null || current_artist.background == null || (background.pixbuf != null && background.pixbuf.width == width && background.pixbuf.height == height)) { 147 | return; 148 | } 149 | if (height < width) { 150 | var pix = current_artist.background.scale_simple (width, width, Gdk.InterpType.BILINEAR); 151 | background.pixbuf = new Gdk.Pixbuf.subpixbuf (pix, 0, (int)(pix.height - height) / 2, width, height); 152 | } else { 153 | var pix = current_artist.background.scale_simple (height, height, Gdk.InterpType.BILINEAR); 154 | background.pixbuf = new Gdk.Pixbuf.subpixbuf (pix, (int)(pix.width - width) / 2, 0, width, height); 155 | } 156 | 157 | artist_name.get_style_context ().add_class ("artist-title"); 158 | artist_sub_title.get_style_context ().add_class ("artist-sub-title"); 159 | } 160 | 161 | private void update_header () { 162 | artist_name.label = current_artist.name; 163 | artist_sub_title.label = _("%d Tracks in %d Album(s)").printf ((int)current_artist.tracks.length (), (int)current_artist.albums.length ()); 164 | } 165 | 166 | public void reset () { 167 | foreach (var child in tracks.get_children ()) { 168 | child.destroy (); 169 | } 170 | background.clear (); 171 | artist_name.label = ""; 172 | artist_sub_title.label = ""; 173 | artist_name.get_style_context ().remove_class ("artist-title"); 174 | artist_sub_title.get_style_context ().remove_class ("artist-sub-title"); 175 | alert_view.show (); 176 | } 177 | 178 | private void add_track (Objects.Track track) { 179 | Idle.add (() => { 180 | var item = new Widgets.Track (track, TrackStyle.ARTIST); 181 | this.tracks.add (item); 182 | update_header (); 183 | if (player.current_track != null && player.current_track.ID == track.ID) { 184 | item.activate (); 185 | } 186 | return false; 187 | }); 188 | } 189 | 190 | private void play_track () { 191 | var selected_row = tracks.get_selected_row (); 192 | if (selected_row != null && !only_mark) { 193 | library_manager.play_track ((selected_row as Widgets.Track).track, Services.PlayMode.ARTIST); 194 | } 195 | } 196 | 197 | public void play_artist () { 198 | Objects.Track? track; 199 | if (settings.shuffle_mode) { 200 | track = current_artist.get_shuffle_track (null); 201 | } else { 202 | track = current_artist.get_first_track (); 203 | } 204 | library_manager.play_track (track, Services.PlayMode.ARTIST); 205 | } 206 | 207 | public void mark_playing_track (Objects.Track? track) { 208 | tracks.unselect_all (); 209 | if (track == null) { 210 | return; 211 | } 212 | foreach (var child in tracks.get_children ()) { 213 | if ((child as Widgets.Track).track.ID == track.ID) { 214 | only_mark = true; 215 | child.activate (); 216 | if (PlayMyMusicApp.instance.mainwindow.content.visible_child_name == "artists") { 217 | child.grab_focus (); 218 | } 219 | only_mark = false; 220 | return; 221 | } 222 | } 223 | } 224 | 225 | private int tracks_sort_func (Gtk.ListBoxRow child1, Gtk.ListBoxRow child2) { 226 | var item1 = (Widgets.Track)child1; 227 | var item2 = (Widgets.Track)child2; 228 | if (item1 != null && item2 != null) { 229 | if (item1.track.album.year != item2.track.album.year) { 230 | return item1.track.album.year - item2.track.album.year; 231 | } 232 | if (item1.track.album.title != item2.track.album.title) { 233 | return item1.track.album.title.collate (item2.track.album.title); 234 | } 235 | if (item1.disc_number != item2.disc_number){ 236 | return item1.disc_number - item2.disc_number; 237 | } 238 | if (item1.track_number != item2.track_number){ 239 | return item1.track_number - item2.track_number; 240 | } 241 | return item1.title.collate (item2.title); 242 | } 243 | return 0; 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/Application.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2019 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic { 29 | public class PlayMyMusicApp : Gtk.Application { 30 | public string DB_PATH { get; private set; } 31 | public string COVER_FOLDER { get; private set; } 32 | public string CACHE_FOLDER { get; private set; } 33 | public string QUEUE_SYS_NAME { get; default = "__queue__";} 34 | 35 | PlayMyMusic.Settings settings; 36 | 37 | static PlayMyMusicApp _instance = null; 38 | public static PlayMyMusicApp instance { 39 | get { 40 | if (_instance == null) { 41 | _instance = new PlayMyMusicApp (); 42 | } 43 | return _instance; 44 | } 45 | } 46 | 47 | [CCode (array_length = false, array_null_terminated = true)] 48 | string[] ? arg_files = null; 49 | 50 | construct { 51 | this.flags |= ApplicationFlags.HANDLES_OPEN; 52 | this.flags |= ApplicationFlags.HANDLES_COMMAND_LINE; 53 | this.application_id = "com.github.artemanufrij.playmymusic"; 54 | settings = Settings.get_default (); 55 | 56 | var action_search_reset = action_generator ("Escape", "search-reset"); 57 | action_search_reset.activate.connect ( 58 | () => { 59 | if (mainwindow != null) { 60 | mainwindow.search_reset (); 61 | } 62 | }); 63 | 64 | var action_show_albums = action_generator ("1", "show-albums"); 65 | action_show_albums.activate.connect ( 66 | () => { 67 | if (mainwindow != null) { 68 | mainwindow.show_view_index (0); 69 | } 70 | }); 71 | 72 | var action_show_artists = action_generator ("2", "show-artists"); 73 | action_show_artists.activate.connect ( 74 | () => { 75 | if (mainwindow != null) { 76 | mainwindow.show_view_index (1); 77 | } 78 | }); 79 | 80 | var action_show_tracks = action_generator ("3", "show-tracks"); 81 | action_show_tracks.activate.connect ( 82 | () => { 83 | if (mainwindow != null) { 84 | mainwindow.show_view_index (2); 85 | } 86 | }); 87 | 88 | var action_show_playlists = action_generator ("4", "show-playlists"); 89 | action_show_playlists.activate.connect ( 90 | () => { 91 | if (mainwindow != null) { 92 | mainwindow.show_view_index (3); 93 | } 94 | }); 95 | 96 | var action_show_radiostations = action_generator ("5", "show-radiostations"); 97 | action_show_radiostations.activate.connect ( 98 | () => { 99 | if (mainwindow != null) { 100 | mainwindow.show_view_index (4); 101 | } 102 | }); 103 | 104 | var action_show_audiocd = action_generator ("6", "show-audiocd"); 105 | action_show_audiocd.activate.connect ( 106 | () => { 107 | if (mainwindow != null) { 108 | mainwindow.show_view_index (5); 109 | } 110 | }); 111 | 112 | var action_quit = action_generator ("q", "quit"); 113 | action_quit.activate.connect ( 114 | () => { 115 | if (mainwindow != null) { 116 | mainwindow.destroy (); 117 | } 118 | }); 119 | 120 | create_cache_folders (); 121 | } 122 | 123 | private SimpleAction action_generator (string command, string action) { 124 | var return_value = new SimpleAction (action, null); 125 | add_action (return_value); 126 | set_accels_for_action ("app.%s".printf (action), {command}); 127 | return return_value; 128 | } 129 | 130 | public void create_cache_folders () { 131 | var library_path = File.new_for_uri (settings.library_location); 132 | if (settings.library_location == "" || !library_path.query_exists ()) { 133 | var music_folder = File.new_for_path (GLib.Environment.get_user_special_dir (GLib.UserDirectory.MUSIC)); 134 | settings.library_location = music_folder.get_uri (); 135 | } 136 | CACHE_FOLDER = GLib.Path.build_filename (GLib.Environment.get_user_cache_dir (), application_id); 137 | try { 138 | File file = File.new_for_path (CACHE_FOLDER); 139 | if (!file.query_exists ()) { 140 | file.make_directory (); 141 | } 142 | } catch (Error e) { 143 | warning (e.message); 144 | } 145 | DB_PATH = GLib.Path.build_filename (CACHE_FOLDER, "database.db"); 146 | 147 | COVER_FOLDER = GLib.Path.build_filename (CACHE_FOLDER, "covers"); 148 | try { 149 | File file = File.new_for_path (COVER_FOLDER); 150 | if (!file.query_exists ()) { 151 | file.make_directory (); 152 | } 153 | } catch (Error e) { 154 | warning (e.message); 155 | } 156 | } 157 | 158 | private PlayMyMusicApp () { 159 | } 160 | 161 | public MainWindow mainwindow { get; private set; default = null; } 162 | 163 | protected override void activate () { 164 | if (mainwindow == null) { 165 | mainwindow = new MainWindow (); 166 | mainwindow.application = this; 167 | Interfaces.MediaKeyListener.listen (); 168 | Interfaces.SoundIndicator.listen (); 169 | } 170 | 171 | mainwindow.present (); 172 | } 173 | 174 | public override void open (File[] files, string hint) { 175 | activate (); 176 | if (files [0].query_exists ()) { 177 | mainwindow.open_file (files [0]); 178 | } 179 | } 180 | 181 | public override int command_line (ApplicationCommandLine cmd) { 182 | command_line_interpreter (cmd); 183 | return 0; 184 | } 185 | 186 | private void command_line_interpreter (ApplicationCommandLine cmd) { 187 | string[] args_cmd = cmd.get_arguments (); 188 | unowned string[] args = args_cmd; 189 | 190 | bool next = false; 191 | bool prev = false; 192 | bool play = false; 193 | 194 | GLib.OptionEntry [] options = new OptionEntry [5]; 195 | options [0] = { "next", 0, 0, OptionArg.NONE, ref next, "Play next track", null }; 196 | options [1] = { "prev", 0, 0, OptionArg.NONE, ref prev, "Play previous track", null }; 197 | options [2] = { "play", 0, 0, OptionArg.NONE, ref play, "Toggle playing", null }; 198 | options [3] = { "", 0, 0, OptionArg.STRING_ARRAY, ref arg_files, null, "[URI…]" }; 199 | options [4] = { null }; 200 | 201 | var opt_context = new OptionContext ("actions"); 202 | opt_context.add_main_entries (options, null); 203 | try { 204 | opt_context.parse (ref args); 205 | } catch (Error err) { 206 | warning (err.message); 207 | return; 208 | } 209 | 210 | if (next || prev || play) { 211 | if (next && mainwindow != null) { 212 | mainwindow.next (); 213 | } else if (prev && mainwindow != null) { 214 | mainwindow.prev (); 215 | } else if (play) { 216 | if (mainwindow == null) { 217 | activate (); 218 | } 219 | mainwindow.toggle_playing (); 220 | } 221 | return; 222 | } 223 | 224 | File[] files = null; 225 | foreach (string arg_file in arg_files) { 226 | if (GLib.FileUtils.test (arg_file, GLib.FileTest.EXISTS)) { 227 | files += (File.new_for_path (arg_file)); 228 | } 229 | } 230 | 231 | if (files != null && files.length > 0) { 232 | open (files, ""); 233 | return; 234 | } 235 | 236 | activate (); 237 | } 238 | 239 | public string get_os_info (string field) { 240 | string return_value = ""; 241 | var file = File.new_for_path ("/etc/os-release"); 242 | try { 243 | var osrel = new Gee.HashMap (); 244 | var dis = new DataInputStream (file.read ()); 245 | string line; 246 | // Read lines until end of file (null) is reached 247 | while ((line = dis.read_line (null)) != null) { 248 | var osrel_component = line.split ("=", 2); 249 | if (osrel_component.length == 2) { 250 | osrel[osrel_component[0]] = osrel_component[1].replace ("\"", ""); 251 | } 252 | } 253 | 254 | return_value = osrel[field]; 255 | } catch (Error e) { 256 | warning ("Couldn't read os-release file, assuming elementary OS"); 257 | } 258 | return return_value; 259 | } 260 | } 261 | } 262 | 263 | public static int main (string [] args) { 264 | Gst.init (ref args); 265 | var app = PlayMyMusic.PlayMyMusicApp.instance; 266 | return app.run (args); 267 | } 268 | -------------------------------------------------------------------------------- /src/Objects/TracksContainer.vala: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2017-2018 Artem Anufrij 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this program. If not, see . 16 | * 17 | * The Noise authors hereby grant permission for non-GPL compatible 18 | * GStreamer plugins to be used and distributed together with GStreamer 19 | * and Noise. This permission is above and beyond the permissions granted 20 | * by the GPL license by which Noise is covered. If you modify this code 21 | * you may extend this exception to your version of the code, but you are not 22 | * obligated to do so. If you do not wish to do so, delete this exception 23 | * statement from your version. 24 | * 25 | * Authored by: Artem Anufrij 26 | */ 27 | 28 | namespace PlayMyMusic.Objects { 29 | public class TracksContainer : GLib.Object { 30 | protected Services.LibraryManager library_manager; 31 | protected Services.DataBaseManager db_manager; 32 | protected Settings settings; 33 | 34 | public signal void track_added (Track track); 35 | public signal void track_removed (Track track); 36 | public signal void cover_changed (); 37 | public signal void background_changed (); 38 | public signal void background_found (); 39 | public signal void removed (); 40 | public signal void updated (); 41 | 42 | public string title { get; set; default = ""; } 43 | public string name { get; set; default = ""; } 44 | protected int _ID = 0; 45 | public int ID { 46 | get { 47 | return _ID; 48 | } set { 49 | _ID = value; 50 | } 51 | } 52 | 53 | public uint64 duration { 54 | get { 55 | uint64 return_value = 0; 56 | foreach (var track in _tracks) { 57 | return_value += track.duration; 58 | } 59 | 60 | return return_value; 61 | } 62 | } 63 | 64 | protected GLib.List _tracks = null; 65 | 66 | public ulong artist_track_added_signal_id { get; set; default = 0; } 67 | 68 | protected bool is_cover_loading = false; 69 | protected bool is_background_loading = false; 70 | 71 | public string background_path { get; protected set; default = ""; } 72 | public string cover_path { get; protected set; default = ""; } 73 | 74 | GLib.List shuffle_index = null; 75 | 76 | public Gdk.Pixbuf ? cover_32 { get; private set; } 77 | 78 | Gdk.Pixbuf ? _cover = null; 79 | public Gdk.Pixbuf ? cover { 80 | get { 81 | return _cover; 82 | } protected set { 83 | _cover = value; 84 | cover_32 = value.scale_simple (32, 32, Gdk.InterpType.BILINEAR); 85 | cover_changed (); 86 | } 87 | } 88 | 89 | Gdk.Pixbuf ? _background = null; 90 | public Gdk.Pixbuf ? background { 91 | get { 92 | if (_background == null && background_path != "") { 93 | if (FileUtils.test (background_path, FileTest.EXISTS)) { 94 | try { 95 | _background = new Gdk.Pixbuf.from_file (background_path); 96 | } catch (Error err) { 97 | warning (err.message); 98 | } 99 | } 100 | } 101 | return _background; 102 | } set { 103 | _background = value; 104 | background_changed (); 105 | } 106 | } 107 | 108 | construct { 109 | settings = Settings.get_default (); 110 | library_manager = Services.LibraryManager.instance; 111 | db_manager = library_manager.db_manager; 112 | 113 | removed.connect ( 114 | () => { 115 | FileUtils.remove (cover_path); 116 | FileUtils.remove (background_path); 117 | }); 118 | } 119 | 120 | public Track ? get_track_by_id (int id) { 121 | Track ? return_value = null; 122 | lock (_tracks) { 123 | foreach (var track in _tracks) { 124 | if (track.ID == id) { 125 | return_value = track; 126 | break; 127 | } 128 | } 129 | } 130 | return return_value; 131 | } 132 | 133 | protected bool has_track (Track track) { 134 | bool return_value = false; 135 | lock (_tracks) { 136 | if (_tracks != null) { 137 | foreach (var t in _tracks) { 138 | if (t.uri == track.uri) { 139 | return_value = true; 140 | break; 141 | } 142 | } 143 | } 144 | } 145 | return return_value; 146 | } 147 | 148 | public Track ? get_next_track (Track current) { 149 | shuffle_index = null; 150 | int i = _tracks.index (current) + 1; 151 | if (i < _tracks.length ()) { 152 | return _tracks.nth_data (i); 153 | } 154 | return null; 155 | } 156 | 157 | public Track ? get_prev_track (Track current) { 158 | shuffle_index = null; 159 | int i = _tracks.index (current) - 1; 160 | if (i > -1) { 161 | return _tracks.nth_data (i); 162 | } 163 | return null; 164 | } 165 | 166 | public Track ? get_shuffle_track (Track ? current) { 167 | if (shuffle_index == null || current == null) { 168 | shuffle_index = new GLib.List (); 169 | } 170 | 171 | if (current != null) { 172 | int i = _tracks.index (current); 173 | shuffle_index.append (i); 174 | } 175 | 176 | if (shuffle_index.length () >= _tracks.length ()) { 177 | shuffle_index = null; 178 | return null; 179 | } 180 | 181 | int r = GLib.Random.int_range (0, (int32)_tracks.length ()); 182 | while (shuffle_index.index (r) != -1) { 183 | r = GLib.Random.int_range (0, (int32)_tracks.length ()); 184 | } 185 | 186 | return _tracks.nth_data (r); 187 | } 188 | 189 | public Track ? get_first_track () { 190 | return _tracks.nth_data (0); 191 | } 192 | 193 | protected void add_track (Track track) { 194 | if (has_track (track)) { 195 | return; 196 | } 197 | lock (_tracks) { 198 | if (this is Playlist) { 199 | this._tracks.insert_sorted_with_data ( 200 | track, 201 | (a, b) => { 202 | return a.track - b.track; 203 | }); 204 | } else if (this is AudioCD) { 205 | this._tracks.append (track); 206 | } else { 207 | this._tracks.insert_sorted_with_data (track, sort_function); 208 | } 209 | } 210 | track_added (track); 211 | } 212 | 213 | private int sort_function (Track a, Track b) { 214 | if (a.album.year != b.album.year) { 215 | return a.album.year - b.album.year; 216 | } 217 | if (a.album.title != b.album.title) { 218 | return a.album.title.collate (b.album.title); 219 | } 220 | if (a.disc != b.disc) { 221 | return a.disc - b.disc; 222 | } 223 | if (a.track != b.track) { 224 | return a.track - b.track; 225 | } 226 | return a.title.collate (b.title); 227 | } 228 | 229 | public void clear_tracks () { 230 | _tracks = new GLib.List (); 231 | } 232 | 233 | public void set_new_cover (Gdk.Pixbuf cover, int size) { 234 | FileUtils.remove (background_path); 235 | this.background = null; 236 | this.cover = save_cover (cover, size); 237 | } 238 | 239 | protected Gdk.Pixbuf ? save_cover (Gdk.Pixbuf p, int size) { 240 | Gdk.Pixbuf ? pixbuf = library_manager.align_and_scale_pixbuf (p, size); 241 | try { 242 | pixbuf.save (cover_path, "jpeg", "quality", "100"); 243 | } catch (Error err) { 244 | warning (err.message); 245 | } 246 | return pixbuf; 247 | } 248 | 249 | public bool has_available_tracks () { 250 | foreach (var track in _tracks) { 251 | if (track.file_exists ()) { 252 | return true; 253 | } 254 | } 255 | return false; 256 | } 257 | 258 | public bool has_tracks () { 259 | return _tracks.length () > 0; 260 | } 261 | 262 | protected void create_background () { 263 | if (this.cover == null || is_background_loading || this.ID == 0) { 264 | return; 265 | } 266 | is_background_loading = true; 267 | 268 | new Thread ( 269 | "create_background", 270 | () => { 271 | if (FileUtils.test (background_path, FileTest.EXISTS)) { 272 | is_background_loading = false; 273 | background_found (); 274 | return null; 275 | } 276 | 277 | double target_size = 1000; 278 | 279 | int width = this.cover.get_width (); 280 | 281 | var surface = new Granite.Drawing.BufferSurface ((int)target_size, (int)target_size); 282 | 283 | double zoom = target_size / (double)width; 284 | 285 | Gdk.cairo_set_source_pixbuf (surface.context, this.cover, 0, 0); 286 | surface.context.scale (zoom, zoom); 287 | surface.context.paint (); 288 | 289 | if (this is AudioCD) { 290 | surface.exponential_blur (32); 291 | } else { 292 | surface.exponential_blur (8); 293 | } 294 | surface.context.paint (); 295 | 296 | surface.surface.write_to_png (this.background_path); 297 | is_background_loading = false; 298 | background_changed (); 299 | return null; 300 | }); 301 | } 302 | } 303 | } 304 | --------------------------------------------------------------------------------