├── .github ├── chrome.svg ├── chromium.svg ├── example.gif ├── firefox.svg ├── opera.svg ├── vivaldi.svg ├── yandex.svg └── ym.svg ├── .gitignore ├── LICENSE ├── README-ru.md ├── README.md ├── chrome_extension ├── _locales │ ├── en │ │ └── messages.json │ └── ru │ │ └── messages.json ├── icons │ ├── 48.png │ └── 96.png ├── manifest.json ├── ymc.js ├── ymc_injection.js └── ymc_injector.js ├── firefox_extension ├── .gitignore ├── _locales │ ├── en │ │ └── messages.json │ └── ru │ │ └── messages.json ├── icons │ ├── 48.png │ └── 96.png ├── manifest.json ├── ymc.js ├── ymc_injection.js └── ymc_injector.js └── native ├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── YMC.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── YMC ├── Application ├── AppDelegate │ ├── AppDelegate+MediaKeyTap.swift │ ├── AppDelegate+Messaging.swift │ ├── AppDelegate+Notifications.swift │ ├── AppDelegate+Permissions.swift │ └── AppDelegate.swift └── PlayerViewController.swift ├── Assets.xcassets ├── AppIcon.appiconset │ ├── 1024.png │ ├── 128.png │ ├── 16.png │ ├── 256-1.png │ ├── 256.png │ ├── 32-1.png │ ├── 32.png │ ├── 512-1.png │ ├── 512.png │ ├── 64.png │ └── Contents.json ├── Contents.json ├── forward.imageset │ ├── Contents.json │ └── fast-forward.pdf ├── heart.imageset │ ├── Contents.json │ └── heart.pdf ├── link.imageset │ ├── Contents.json │ └── share.pdf ├── logo.imageset │ ├── Contents.json │ └── logo.pdf ├── logo_large.imageset │ ├── Contents.json │ └── logo_large.pdf ├── pause.imageset │ ├── Contents.json │ └── pause.pdf ├── play.imageset │ ├── Contents.json │ └── play.pdf ├── refresh.imageset │ ├── Contents.json │ └── refresh.pdf ├── rewind.imageset │ ├── Contents.json │ └── rewind.pdf └── settings.imageset │ ├── Contents.json │ └── settings.pdf ├── Base.lproj └── Main.storyboard ├── Classes ├── Logger.swift ├── Manifest.swift ├── Messaging.swift └── Player.swift ├── Info.plist ├── Vendor └── BinUtils.swift └── YMC.entitlements /.github/chrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /.github/chromium.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 53 | 55 | 59 | 63 | 67 | 68 | 71 | 75 | 79 | 80 | 82 | 86 | 90 | 94 | 95 | 97 | 101 | 105 | 109 | 110 | 112 | 116 | 120 | 124 | 125 | 128 | 132 | 136 | 137 | 140 | 144 | 148 | 149 | 160 | 169 | 180 | 191 | 202 | 212 | 221 | 222 | 232 | 237 | 242 | 247 | 252 | 257 | 262 | 267 | 272 | 277 | 278 | -------------------------------------------------------------------------------- /.github/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/.github/example.gif -------------------------------------------------------------------------------- /.github/firefox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | firefox-logo 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /.github/opera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/vivaldi.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/yandex.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 46 | 48 | 55 | 58 | 62 | 63 | 64 | 71 | 76 | 77 | -------------------------------------------------------------------------------- /.github/ym.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Brand / Icon / Simple / SimpleIcon@3x 5 | Created with Sketch. 6 | 7 | 8 | 9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | dist 4 | node_modules 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maksim Karelov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-ru.md: -------------------------------------------------------------------------------- 1 |

2 | English version 3 |

4 | 5 |

6 | Yandex Music Logo 7 |
8 | Yandex.Music Control 9 |

10 | 11 |
12 | Управляйте Яндекс.Музыкой из любого окна macOS 13 |
14 |
15 |   16 |   17 |   18 |   19 |   20 | 21 |
22 | 23 |

24 | Возможности • 25 | TODO • 26 | Горячие клавиши • 27 | Установка • 28 | Отладка • 29 | Сторонние библиотеки • 30 | Лицензия 31 |

32 | 33 |

34 | Example GIF 35 |

36 | 37 | ## Возможности 38 | 39 | - Управляйте воспроизведением из любого окна или экрана при помощи виджета в верхней панели 40 | - Управляйте воспроизведением при помощи стандартных медиа-клавиш Мак-устройства 41 | - Получайте информацию о текущей композиции при помощи уведомления 42 | - Делитесь ссылкой на текущую композицию через системное меню "Поделиться" 43 | 44 | ## TODO 45 | 46 | - [ ] Обрабатывать нажатия на кнопки гарнитур 47 | - [ ] Обрабатывать другие домены (не только music.yandex.ru) 48 | 49 | ## Горячие клавиши 50 | 51 | | **Функции** | **Горячие клавиши** | 52 | |-----------------------------|---------------------------------------------------------| 53 | | **Play** | Воспроизведение/Пауза | 54 | | **Next** | Включить следующую композицию | 55 | | **Prev** | Включить предыдущую композицию | 56 | | **⌘** + **Play** | Показать информацию о текущей композиции | 57 | | **⌘** + **Next** | Поставить/Снять отмеку "Нравится" с текущей композиции | 58 | | **⌘** + **Prev** | Скопировать ссылки на текущую композицию | 59 | 60 | ## Установка 61 | 62 | - Скачайте и установите расширение для Вашего любимого браузера со [страницы релизов](https://github.com/Ty3uK/YMC/releases) 63 | - Скачайте приложение со [страницы релизов](https://github.com/Ty3uK/YMC/releases) и скопируйте в папку `Программы` 64 | - Запустите приложение и дождитесь сообщения `Manifest updated`, нажмите `Got it` 65 | - Приложение запустится автоматически при посещении `music.yandex.ru` 66 | 67 | Если Вы хотите управлять воспроизведением при помощи стандартных медиа-клавиш - включите браузер в настройках универсального доступа (`Системные настройки → Защита и безопасность → Универсальный доступ`) и обновите вкладку яндекс.музыки в браузере. 68 | 69 | #### Note 70 | 71 | В случае ошибки `The application can't be opened` или `App is damaged and can't be opened` запустите следующую команду в терминале: 72 | 73 | ```bash 74 | sudo spctl --master-disable 75 | ``` 76 | 77 | ## Отладка 78 | 79 | При включенной настройке `Enable debugging` приложение записывает лог в файл `/tmp/ymc.log`. Пожалуйста, прикладывайте файл лога при создании ишью. 80 | 81 | ## Сторонние библиотеки 82 | 83 | - [RxSwift](https://github.com/ReactiveX/RxSwift) 84 | - [SwiftyBeaver](https://github.com/SwiftyBeaver/SwiftyBeaver) 85 | - [MediaKeyTap](https://github.com/nhurden/MediaKeyTap) ([fork](https://github.com/Ty3uK/MediaKeyTap)) 86 | - [Files](https://github.com/JohnSundell/Files) 87 | - [BinUtils](https://github.com/nst/BinUtils) 88 | 89 | ## Лицензия 90 | 91 | [MIT](LICENSE) 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Русская версия 3 |

4 | 5 |

6 | Yandex Music Logo 7 |
8 | Yandex.Music Control 9 |

10 | 11 |
12 | Manage Yandex.Music from any macOS window 13 |
14 |
15 |   16 |   17 |   18 |   19 |   20 | 21 |
22 | 23 |

24 | Features • 25 | TODO • 26 | Hotkeys • 27 | Install • 28 | Debug • 29 | External libraries • 30 | License 31 |

32 | 33 |

34 | Example GIF 35 |

36 | 37 | ## Features 38 | 39 | - Control playback from any window or screen using the widget in the top panel 40 | - Control playback with Mac native media keys 41 | - Get current track info with notification 42 | - Share link to track through native sharing menu 43 | 44 | ## TODO 45 | 46 | - [ ] Handle headphones media keys 47 | - [ ] Handle other domains (not only music.yandex.ru) 48 | 49 | ## Hotkeys 50 | 51 | | **Function** | **Hotkey** | 52 | |-----------------------------|----------------------------| 53 | | **Play** | Play/Pause track | 54 | | **Next** | Switch to next track | 55 | | **Prev** | Switch to previous track | 56 | | **⌘** + **Play** | Show current track info | 57 | | **⌘** + **Next** | Like/Unlike current track | 58 | | **⌘** + **Prev** | Copy current track link | 59 | 60 | ## Install 61 | 62 | - Download and install extension for your favorite browser from [release page](https://github.com/Ty3uK/YMC/releases) 63 | - Download application from [release page](https://github.com/Ty3uK/YMC/releases) and copy to `Applications` folder 64 | - Run application and wait for message `Manifest updated`, click `Got it` 65 | - When you visit `music.yandex.ru` application will be started automatically 66 | 67 | If you want to control playback with native media keys - enable your browser in accessibility settings (`System Preferences → Security & Privacy → Accessibility`) and reload yandex music tab in browser 68 | 69 | #### Note 70 | 71 | If you get errors like `The application can't be opened` or `App is damaged and can't be opened` run this command in terminal: 72 | 73 | ```bash 74 | sudo spctl --master-disable 75 | ``` 76 | 77 | ## Debug 78 | 79 | When `Enable debugging` checkbox is checked, application will write log file to `/tmp/ymc.log`. Attach log file in issue, please. 80 | 81 | ## External libraries 82 | 83 | - [RxSwift](https://github.com/ReactiveX/RxSwift) 84 | - [SwiftyBeaver](https://github.com/SwiftyBeaver/SwiftyBeaver) 85 | - [MediaKeyTap](https://github.com/nhurden/MediaKeyTap) ([fork](https://github.com/Ty3uK/MediaKeyTap)) 86 | - [Files](https://github.com/JohnSundell/Files) 87 | - [BinUtils](https://github.com/nst/BinUtils) 88 | 89 | ## License 90 | 91 | [MIT](LICENSE) 92 | -------------------------------------------------------------------------------- /chrome_extension/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Yandex.Music Control" 4 | }, 5 | "extensionDescription": { 6 | "message": "Control Yandex.Music from any screen in MacOS" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chrome_extension/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Yandex.Music Control" 4 | }, 5 | "extensionDescription": { 6 | "message": "Управляйте Яндекс.Музыкой с любого экрана в MacOS" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chrome_extension/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/chrome_extension/icons/48.png -------------------------------------------------------------------------------- /chrome_extension/icons/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/chrome_extension/icons/96.png -------------------------------------------------------------------------------- /chrome_extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "description": "__MSG_extensionDescription__", 5 | "version": "1.1.0", 6 | 7 | "default_locale": "en", 8 | 9 | "icons": { 10 | "48": "icons/48.png", 11 | "96": "icons/96.png" 12 | }, 13 | 14 | "web_accessible_resources": [ 15 | "ymc_injection.js" 16 | ], 17 | 18 | "background": { 19 | "scripts": ["ymc.js"] 20 | }, 21 | 22 | "content_scripts": [ 23 | { 24 | "matches": ["*://music.yandex.ru/*", "*://radio.yandex.ru/*"], 25 | "js": ["ymc_injector.js"] 26 | } 27 | ], 28 | 29 | "permissions": [ 30 | "*://music.yandex.ru/*", 31 | "*://radio.yandex.ru/*", 32 | "tabs", 33 | "nativeMessaging", 34 | "notifications", 35 | "clipboardWrite" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /chrome_extension/ymc.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let page; 3 | let app; 4 | 5 | const messageQueue = []; 6 | 7 | async function onConnect(port) { 8 | let tabs = (await queryTabs({ url: '*://*.yandex.ru/*' })) || []; 9 | 10 | tabs = tabs.filter(tab => tab.url.indexOf('music.') !== -1 || tab.url.indexOf('radio.') !== -1); 11 | 12 | if (tabs.length > 1) { 13 | return; 14 | } 15 | 16 | page = port; 17 | app = chrome.runtime.connectNative('ymc'); 18 | 19 | port.onMessage.addListener(message => { 20 | messageQueue.push(message); 21 | 22 | const messageFromQueue = messageQueue.shift(); 23 | 24 | if (messageFromQueue) { 25 | app.postMessage(message); 26 | } 27 | }); 28 | 29 | port.onDisconnect.addListener(() => app.disconnect()); 30 | 31 | app.onMessage.addListener(message => page.postMessage(message)); 32 | } 33 | 34 | chrome.runtime.onConnect.addListener(onConnect); 35 | 36 | async function queryTabs(options) { 37 | return new Promise(resolve => chrome.tabs.query(options, resolve)); 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /chrome_extension/ymc_injection.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const scriptElement = document.querySelector('script[data-ymc-id]'); 3 | const id = scriptElement && scriptElement.getAttribute('data-ymc-id'); 4 | 5 | if (!id) { 6 | return; 7 | } 8 | 9 | let isPlaying; 10 | 11 | externalAPI.on(externalAPI.EVENT_READY, sendCurrentTrack); 12 | 13 | externalAPI.on(externalAPI.EVENT_TRACK, sendCurrentTrack); 14 | 15 | externalAPI.on(externalAPI.EVENT_CONTROLS, () => { 16 | postMessage('CONTROLS', externalAPI.getControls()); 17 | postMessage('TOGGLE_LIKE', { state: getLikedState() }); 18 | }); 19 | 20 | externalAPI.on(externalAPI.EVENT_STATE, () => { 21 | const playingState = externalAPI.isPlaying(); 22 | 23 | if (isPlaying !== playingState) { 24 | isPlaying = playingState; 25 | postMessage('PLAYING', { state: isPlaying }); 26 | } 27 | }); 28 | 29 | window.addEventListener('message', event => { 30 | if (event.source !== window || !(event.data.type && event.data.type === 'YMC_EXT')) { 31 | return; 32 | } 33 | 34 | const message = event.data.data; 35 | const type = message.type; 36 | 37 | if (type === 'PLAY_PAUSE') { 38 | externalAPI.togglePause(); 39 | } else if (type === 'PREV') { 40 | externalAPI.prev(); 41 | } else if (type === 'NEXT') { 42 | externalAPI.next(); 43 | } else if (type === 'TOGGLE_LIKE') { 44 | externalAPI.toggleLike().then(success => { 45 | if (!success) { 46 | return; 47 | } 48 | 49 | postMessage(type, { state: getLikedState() }); 50 | }); 51 | } else if (type === 'REFRESH') { 52 | postMessage('PLAYER_STATE', { 53 | currentTrack: getTrackInfo(), 54 | controls: externalAPI.getControls(), 55 | isPlaying: externalAPI.isPlaying() 56 | }); 57 | } 58 | }); 59 | 60 | function sendCurrentTrack() { 61 | postMessage('CURRENT_TRACK', getTrackInfo()); 62 | } 63 | 64 | function getTrackInfo() { 65 | const trackInfo = externalAPI.getCurrentTrack(); 66 | 67 | if (!trackInfo) { 68 | return {}; 69 | } 70 | 71 | return { 72 | title: trackInfo.title, 73 | artist: trackInfo.artists.map(it => it.title).join(', '), 74 | cover: `https://${trackInfo.cover.slice(0, -3)}/400x400`, 75 | liked: trackInfo.liked, 76 | link: `${location.origin}${trackInfo.link}` 77 | } 78 | } 79 | 80 | function getLikedState() { 81 | const trackInfo = externalAPI.getCurrentTrack(); 82 | return !!(trackInfo && trackInfo.liked); 83 | } 84 | 85 | function postMessage(type, data) { 86 | window.postMessage({ type: 'YMC_PAGE', data: { type: type, data: data }}, '*'); 87 | } 88 | })(); 89 | -------------------------------------------------------------------------------- /chrome_extension/ymc_injector.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const id = chrome.runtime.id; 3 | const url = chrome.runtime.getURL('ymc_injection.js'); 4 | 5 | let script = document.querySelector(`script[data-ymc-id='${id}']`); 6 | 7 | if (script) { 8 | return; 9 | } 10 | 11 | script = document.createElement('script'); 12 | script.src = url; 13 | script.setAttribute('data-ymc-id', id); 14 | 15 | document.head.appendChild(script); 16 | 17 | const port = chrome.runtime.connect({ name: 'ymc' }); 18 | 19 | port.onMessage.addListener(message => { 20 | window.postMessage({ type: 'YMC_EXT', data: message }, '*'); 21 | }); 22 | 23 | window.addEventListener('message', event => { 24 | if (event.source !== window || !(event.data.type && event.data.type === 'YMC_PAGE')) { 25 | return; 26 | } 27 | 28 | port.postMessage(event.data.data); 29 | }); 30 | })(); 31 | -------------------------------------------------------------------------------- /firefox_extension/.gitignore: -------------------------------------------------------------------------------- 1 | .web-extension-id 2 | web-ext-artifacts 3 | 4 | -------------------------------------------------------------------------------- /firefox_extension/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Yandex.Music Control" 4 | }, 5 | "extensionDescription": { 6 | "message": "Control Yandex.Music from any screen in MacOS" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /firefox_extension/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Yandex.Music Control" 4 | }, 5 | "extensionDescription": { 6 | "message": "Управляйте Яндекс.Музыкой с любого экрана в MacOS" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /firefox_extension/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/firefox_extension/icons/48.png -------------------------------------------------------------------------------- /firefox_extension/icons/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/firefox_extension/icons/96.png -------------------------------------------------------------------------------- /firefox_extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "description": "__MSG_extensionDescription__", 5 | "version": "1.1.0", 6 | 7 | "default_locale": "en", 8 | 9 | "icons": { 10 | "48": "icons/48.png", 11 | "96": "icons/96.png" 12 | }, 13 | 14 | "applications": { 15 | "gecko": { 16 | "id": "ymc@karelov.info", 17 | "strict_min_version": "54.0" 18 | } 19 | }, 20 | 21 | "web_accessible_resources": [ 22 | "ymc_injection.js" 23 | ], 24 | 25 | "background": { 26 | "scripts": ["ymc.js"] 27 | }, 28 | 29 | "content_scripts": [ 30 | { 31 | "matches": ["*://music.yandex.ru/*", "*://radio.yandex.ru/*"], 32 | "js": ["ymc_injector.js"] 33 | } 34 | ], 35 | 36 | "permissions": [ 37 | "*://music.yandex.ru/*", 38 | "*://radio.yandex.ru/*", 39 | "tabs", 40 | "nativeMessaging" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /firefox_extension/ymc.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let page; 3 | let app; 4 | 5 | const messageQueue = []; 6 | 7 | async function onConnect(port) { 8 | let tabs = (await browser.tabs.query({ url: '*://*.yandex.ru/*' })) || []; 9 | 10 | tabs = tabs.filter(tab => tab.url.indexOf('music.') !== -1 || tab.url.indexOf('radio.') !== -1) 11 | 12 | if (tabs.length > 1) { 13 | return; 14 | } 15 | 16 | page = port; 17 | app = browser.runtime.connectNative('ymc'); 18 | 19 | port.onMessage.addListener(message => { 20 | messageQueue.push(message); 21 | 22 | const messageFromQueue = messageQueue.shift(); 23 | 24 | if (messageFromQueue) { 25 | app.postMessage(message); 26 | } 27 | }); 28 | 29 | port.onDisconnect.addListener(() => app.disconnect()); 30 | 31 | app.onMessage.addListener(message => page.postMessage(message)); 32 | } 33 | 34 | browser.runtime.onConnect.addListener(onConnect); 35 | })(); 36 | -------------------------------------------------------------------------------- /firefox_extension/ymc_injection.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const scriptElement = document.querySelector('script[data-ymc-id]'); 3 | const id = scriptElement && scriptElement.getAttribute('data-ymc-id'); 4 | 5 | if (!id) { 6 | return; 7 | } 8 | 9 | let isPlaying; 10 | 11 | externalAPI.on(externalAPI.EVENT_READY, sendCurrentTrack); 12 | 13 | externalAPI.on(externalAPI.EVENT_TRACK, sendCurrentTrack); 14 | 15 | externalAPI.on(externalAPI.EVENT_CONTROLS, () => { 16 | postMessage('CONTROLS', externalAPI.getControls()); 17 | postMessage('TOGGLE_LIKE', { state: getLikedState() }); 18 | }); 19 | 20 | externalAPI.on(externalAPI.EVENT_STATE, () => { 21 | const playingState = externalAPI.isPlaying(); 22 | 23 | if (isPlaying !== playingState) { 24 | isPlaying = playingState; 25 | postMessage('PLAYING', { state: isPlaying }); 26 | } 27 | }); 28 | 29 | window.addEventListener('message', event => { 30 | if (event.source !== window || !(event.data.type && event.data.type === 'YMC_EXT')) { 31 | return; 32 | } 33 | 34 | const message = event.data.data; 35 | const type = message.type; 36 | 37 | if (type === 'PLAY_PAUSE') { 38 | externalAPI.togglePause(); 39 | } else if (type === 'PREV') { 40 | externalAPI.prev(); 41 | } else if (type === 'NEXT') { 42 | externalAPI.next(); 43 | } else if (type === 'TOGGLE_LIKE') { 44 | externalAPI.toggleLike().then(success => { 45 | if (!success) { 46 | return; 47 | } 48 | 49 | postMessage(type, { state: getLikedState() }); 50 | }); 51 | } else if (type === 'REFRESH') { 52 | postMessage('PLAYER_STATE', { 53 | currentTrack: getTrackInfo(), 54 | controls: externalAPI.getControls(), 55 | isPlaying: externalAPI.isPlaying() 56 | }); 57 | } 58 | }); 59 | 60 | function sendCurrentTrack() { 61 | postMessage('CURRENT_TRACK', getTrackInfo()); 62 | } 63 | 64 | function getTrackInfo() { 65 | const trackInfo = externalAPI.getCurrentTrack(); 66 | 67 | if (!trackInfo) { 68 | return {}; 69 | } 70 | 71 | return { 72 | title: trackInfo.title, 73 | artist: trackInfo.artists.map(it => it.title).join(', '), 74 | cover: `https://${trackInfo.cover.slice(0, -3)}/400x400`, 75 | liked: trackInfo.liked, 76 | link: `${location.origin}${trackInfo.link}` 77 | } 78 | } 79 | 80 | function getLikedState() { 81 | const trackInfo = externalAPI.getCurrentTrack(); 82 | return !!(trackInfo && trackInfo.liked); 83 | } 84 | 85 | function postMessage(type, data) { 86 | window.postMessage({ type: 'YMC_PAGE', data: { type: type, data: data }}, '*'); 87 | } 88 | })(); 89 | -------------------------------------------------------------------------------- /firefox_extension/ymc_injector.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const id = browser.runtime.id; 3 | const url = browser.runtime.getURL('ymc_injection.js'); 4 | 5 | let script = document.querySelector(`script[data-ymc-id='${id}']`); 6 | 7 | if (script) { 8 | return; 9 | } 10 | 11 | script = document.createElement('script'); 12 | script.src = url; 13 | script.setAttribute('data-ymc-id', id); 14 | 15 | document.head.appendChild(script); 16 | 17 | const port = browser.runtime.connect({ name: 'ymc' }); 18 | 19 | port.onMessage.addListener(message => { 20 | window.postMessage({ type: 'YMC_EXT', data: message }, '*'); 21 | }); 22 | 23 | window.addEventListener('message', event => { 24 | if (event.source !== window || !(event.data.type && event.data.type === 'YMC_PAGE')) { 25 | return; 26 | } 27 | 28 | port.postMessage(event.data.data); 29 | }); 30 | })(); 31 | -------------------------------------------------------------------------------- /native/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | Carthage/Checkouts 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots/**/*.png 69 | fastlane/test_output 70 | 71 | -------------------------------------------------------------------------------- /native/Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveX/RxSwift" ~> 4.0 2 | github "SwiftyBeaver/SwiftyBeaver" 3 | github "Ty3uK/MediaKeyTap" "master" 4 | github "JohnSundell/Files" 5 | 6 | -------------------------------------------------------------------------------- /native/Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "JohnSundell/Files" "3.0.0" 2 | github "ReactiveX/RxSwift" "4.4.2" 3 | github "SwiftyBeaver/SwiftyBeaver" "1.7.0" 4 | github "Ty3uK/MediaKeyTap" "066ab19ffb1d44a02780f058af33a6155550e8ab" 5 | -------------------------------------------------------------------------------- /native/YMC.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CE0E362F2212898700877823 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0E362E2212898700877823 /* AppDelegate.swift */; }; 11 | CE0E36332212898A00877823 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE0E36322212898A00877823 /* Assets.xcassets */; }; 12 | CE0E36362212898A00877823 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE0E36342212898A00877823 /* Main.storyboard */; }; 13 | CE19E05722226F2B0047B234 /* AppDelegate+MediaKeyTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19E05622226F2B0047B234 /* AppDelegate+MediaKeyTap.swift */; }; 14 | CE19E05922226FA60047B234 /* AppDelegate+Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19E05822226FA60047B234 /* AppDelegate+Permissions.swift */; }; 15 | CE19E05B22226FDF0047B234 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19E05A22226FDF0047B234 /* AppDelegate+Notifications.swift */; }; 16 | CE19E05D222270430047B234 /* AppDelegate+Messaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19E05C222270430047B234 /* AppDelegate+Messaging.swift */; }; 17 | CE8623A4221BD1D400AEEF59 /* BinUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8623A3221BD1D400AEEF59 /* BinUtils.swift */; }; 18 | CE8759722217390A008C8B5C /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8759712217390A008C8B5C /* Player.swift */; }; 19 | CE89AC772213EEC20028760B /* Messaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE89AC752213EEC20028760B /* Messaging.swift */; }; 20 | CEC02744221E8A1A007B32EE /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC02743221E8A1A007B32EE /* Logger.swift */; }; 21 | CECAAF7B221BD47900D17EC5 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CECAAF79221BD47900D17EC5 /* RxSwift.framework */; }; 22 | CECAAF7C221BD47900D17EC5 /* RxSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CECAAF79221BD47900D17EC5 /* RxSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 23 | CECAAF7D221BD47900D17EC5 /* RxAtomic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CECAAF7A221BD47900D17EC5 /* RxAtomic.framework */; }; 24 | CECAAF7E221BD47900D17EC5 /* RxAtomic.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CECAAF7A221BD47900D17EC5 /* RxAtomic.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 25 | CECAAF82221BD48400D17EC5 /* SwiftyBeaver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CECAAF80221BD48400D17EC5 /* SwiftyBeaver.framework */; }; 26 | CECAAF83221BD48400D17EC5 /* SwiftyBeaver.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CECAAF80221BD48400D17EC5 /* SwiftyBeaver.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 27 | CECAAF84221BD48400D17EC5 /* MediaKeyTap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CECAAF81221BD48400D17EC5 /* MediaKeyTap.framework */; }; 28 | CECAAF85221BD48400D17EC5 /* MediaKeyTap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CECAAF81221BD48400D17EC5 /* MediaKeyTap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 29 | CECAAF88221BD5C400D17EC5 /* RxSwift.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECAAF87221BD5C400D17EC5 /* RxSwift.framework.dSYM */; }; 30 | CECAAF8A221BD5E300D17EC5 /* RxAtomic.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECAAF89221BD5E300D17EC5 /* RxAtomic.framework.dSYM */; }; 31 | CECAAF8C221BD5F300D17EC5 /* MediaKeyTap.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECAAF8B221BD5F300D17EC5 /* MediaKeyTap.framework.dSYM */; }; 32 | CECAAF8E221BD5FA00D17EC5 /* SwiftyBeaver.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECAAF8D221BD5FA00D17EC5 /* SwiftyBeaver.framework.dSYM */; }; 33 | CEDB4D2C221D1F2E0012D50D /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDB4D2B221D1F2E0012D50D /* Manifest.swift */; }; 34 | CEDB4D33221D26120012D50D /* Files.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = CEDB4D32221D26120012D50D /* Files.framework.dSYM */; }; 35 | CEDB4D35221D277E0012D50D /* Files.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEDB4D34221D277E0012D50D /* Files.framework */; }; 36 | CEDB4D36221D277E0012D50D /* Files.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEDB4D34221D277E0012D50D /* Files.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 37 | CEE809D922128B3200686E75 /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE809D822128B3200686E75 /* PlayerViewController.swift */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXCopyFilesBuildPhase section */ 41 | CECAAF7F221BD47900D17EC5 /* Embed Frameworks */ = { 42 | isa = PBXCopyFilesBuildPhase; 43 | buildActionMask = 2147483647; 44 | dstPath = ""; 45 | dstSubfolderSpec = 10; 46 | files = ( 47 | CECAAF7C221BD47900D17EC5 /* RxSwift.framework in Embed Frameworks */, 48 | CECAAF83221BD48400D17EC5 /* SwiftyBeaver.framework in Embed Frameworks */, 49 | CECAAF7E221BD47900D17EC5 /* RxAtomic.framework in Embed Frameworks */, 50 | CEDB4D36221D277E0012D50D /* Files.framework in Embed Frameworks */, 51 | CECAAF85221BD48400D17EC5 /* MediaKeyTap.framework in Embed Frameworks */, 52 | ); 53 | name = "Embed Frameworks"; 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | CECAAF86221BD5A400D17EC5 /* CopyFiles */ = { 57 | isa = PBXCopyFilesBuildPhase; 58 | buildActionMask = 2147483647; 59 | dstPath = ""; 60 | dstSubfolderSpec = 16; 61 | files = ( 62 | CEDB4D33221D26120012D50D /* Files.framework.dSYM in CopyFiles */, 63 | CECAAF88221BD5C400D17EC5 /* RxSwift.framework.dSYM in CopyFiles */, 64 | CECAAF8A221BD5E300D17EC5 /* RxAtomic.framework.dSYM in CopyFiles */, 65 | CECAAF8C221BD5F300D17EC5 /* MediaKeyTap.framework.dSYM in CopyFiles */, 66 | CECAAF8E221BD5FA00D17EC5 /* SwiftyBeaver.framework.dSYM in CopyFiles */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | /* End PBXCopyFilesBuildPhase section */ 71 | 72 | /* Begin PBXFileReference section */ 73 | CE0E362B2212898700877823 /* YMC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = YMC.app; sourceTree = BUILT_PRODUCTS_DIR; }; 74 | CE0E362E2212898700877823 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 75 | CE0E36322212898A00877823 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 76 | CE0E36352212898A00877823 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 77 | CE0E36372212898A00877823 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 78 | CE0E36382212898A00877823 /* YMC.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = YMC.entitlements; sourceTree = ""; }; 79 | CE19E05622226F2B0047B234 /* AppDelegate+MediaKeyTap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MediaKeyTap.swift"; sourceTree = ""; }; 80 | CE19E05822226FA60047B234 /* AppDelegate+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Permissions.swift"; sourceTree = ""; }; 81 | CE19E05A22226FDF0047B234 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = ""; }; 82 | CE19E05C222270430047B234 /* AppDelegate+Messaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Messaging.swift"; sourceTree = ""; }; 83 | CE8623A3221BD1D400AEEF59 /* BinUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinUtils.swift; sourceTree = ""; }; 84 | CE8759712217390A008C8B5C /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; 85 | CE89AC752213EEC20028760B /* Messaging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Messaging.swift; sourceTree = ""; }; 86 | CEC02743221E8A1A007B32EE /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 87 | CECAAF79221BD47900D17EC5 /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = Carthage/Build/Mac/RxSwift.framework; sourceTree = ""; }; 88 | CECAAF7A221BD47900D17EC5 /* RxAtomic.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxAtomic.framework; path = Carthage/Build/Mac/RxAtomic.framework; sourceTree = ""; }; 89 | CECAAF80221BD48400D17EC5 /* SwiftyBeaver.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftyBeaver.framework; path = Carthage/Build/Mac/SwiftyBeaver.framework; sourceTree = ""; }; 90 | CECAAF81221BD48400D17EC5 /* MediaKeyTap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaKeyTap.framework; path = Carthage/Build/Mac/MediaKeyTap.framework; sourceTree = ""; }; 91 | CECAAF87221BD5C400D17EC5 /* RxSwift.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = RxSwift.framework.dSYM; path = Carthage/Build/Mac/RxSwift.framework.dSYM; sourceTree = ""; }; 92 | CECAAF89221BD5E300D17EC5 /* RxAtomic.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = RxAtomic.framework.dSYM; path = Carthage/Build/Mac/RxAtomic.framework.dSYM; sourceTree = ""; }; 93 | CECAAF8B221BD5F300D17EC5 /* MediaKeyTap.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = MediaKeyTap.framework.dSYM; path = Carthage/Build/Mac/MediaKeyTap.framework.dSYM; sourceTree = ""; }; 94 | CECAAF8D221BD5FA00D17EC5 /* SwiftyBeaver.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = SwiftyBeaver.framework.dSYM; path = Carthage/Build/Mac/SwiftyBeaver.framework.dSYM; sourceTree = ""; }; 95 | CEDB4D2B221D1F2E0012D50D /* Manifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Manifest.swift; sourceTree = ""; }; 96 | CEDB4D32221D26120012D50D /* Files.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = Files.framework.dSYM; path = Carthage/Build/Mac/Files.framework.dSYM; sourceTree = ""; }; 97 | CEDB4D34221D277E0012D50D /* Files.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Files.framework; path = Carthage/Build/Mac/Files.framework; sourceTree = ""; }; 98 | CEE809D822128B3200686E75 /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 99 | /* End PBXFileReference section */ 100 | 101 | /* Begin PBXFrameworksBuildPhase section */ 102 | CE0E36282212898700877823 /* Frameworks */ = { 103 | isa = PBXFrameworksBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | CECAAF7B221BD47900D17EC5 /* RxSwift.framework in Frameworks */, 107 | CECAAF82221BD48400D17EC5 /* SwiftyBeaver.framework in Frameworks */, 108 | CECAAF7D221BD47900D17EC5 /* RxAtomic.framework in Frameworks */, 109 | CEDB4D35221D277E0012D50D /* Files.framework in Frameworks */, 110 | CECAAF84221BD48400D17EC5 /* MediaKeyTap.framework in Frameworks */, 111 | ); 112 | runOnlyForDeploymentPostprocessing = 0; 113 | }; 114 | /* End PBXFrameworksBuildPhase section */ 115 | 116 | /* Begin PBXGroup section */ 117 | CE0E36222212898700877823 = { 118 | isa = PBXGroup; 119 | children = ( 120 | CEDB4D34221D277E0012D50D /* Files.framework */, 121 | CEDB4D32221D26120012D50D /* Files.framework.dSYM */, 122 | CECAAF7A221BD47900D17EC5 /* RxAtomic.framework */, 123 | CECAAF89221BD5E300D17EC5 /* RxAtomic.framework.dSYM */, 124 | CECAAF79221BD47900D17EC5 /* RxSwift.framework */, 125 | CECAAF87221BD5C400D17EC5 /* RxSwift.framework.dSYM */, 126 | CECAAF81221BD48400D17EC5 /* MediaKeyTap.framework */, 127 | CECAAF8B221BD5F300D17EC5 /* MediaKeyTap.framework.dSYM */, 128 | CECAAF80221BD48400D17EC5 /* SwiftyBeaver.framework */, 129 | CECAAF8D221BD5FA00D17EC5 /* SwiftyBeaver.framework.dSYM */, 130 | CE0E362D2212898700877823 /* YMC */, 131 | CE0E362C2212898700877823 /* Products */, 132 | ); 133 | sourceTree = ""; 134 | }; 135 | CE0E362C2212898700877823 /* Products */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | CE0E362B2212898700877823 /* YMC.app */, 139 | ); 140 | name = Products; 141 | sourceTree = ""; 142 | }; 143 | CE0E362D2212898700877823 /* YMC */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | CE0E36342212898A00877823 /* Main.storyboard */, 147 | CE0E36382212898A00877823 /* YMC.entitlements */, 148 | CE0E36322212898A00877823 /* Assets.xcassets */, 149 | CE0E36372212898A00877823 /* Info.plist */, 150 | CE8623A0221BD17500AEEF59 /* Application */, 151 | CE8623A1221BD18C00AEEF59 /* Classes */, 152 | CE8623A2221BD1A500AEEF59 /* Vendor */, 153 | ); 154 | path = YMC; 155 | sourceTree = ""; 156 | }; 157 | CE19E05522226F020047B234 /* AppDelegate */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | CE0E362E2212898700877823 /* AppDelegate.swift */, 161 | CE19E05622226F2B0047B234 /* AppDelegate+MediaKeyTap.swift */, 162 | CE19E05822226FA60047B234 /* AppDelegate+Permissions.swift */, 163 | CE19E05A22226FDF0047B234 /* AppDelegate+Notifications.swift */, 164 | CE19E05C222270430047B234 /* AppDelegate+Messaging.swift */, 165 | ); 166 | path = AppDelegate; 167 | sourceTree = ""; 168 | }; 169 | CE8623A0221BD17500AEEF59 /* Application */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | CE19E05522226F020047B234 /* AppDelegate */, 173 | CEE809D822128B3200686E75 /* PlayerViewController.swift */, 174 | ); 175 | path = Application; 176 | sourceTree = ""; 177 | }; 178 | CE8623A1221BD18C00AEEF59 /* Classes */ = { 179 | isa = PBXGroup; 180 | children = ( 181 | CE89AC752213EEC20028760B /* Messaging.swift */, 182 | CE8759712217390A008C8B5C /* Player.swift */, 183 | CEDB4D2B221D1F2E0012D50D /* Manifest.swift */, 184 | CEC02743221E8A1A007B32EE /* Logger.swift */, 185 | ); 186 | path = Classes; 187 | sourceTree = ""; 188 | }; 189 | CE8623A2221BD1A500AEEF59 /* Vendor */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | CE8623A3221BD1D400AEEF59 /* BinUtils.swift */, 193 | ); 194 | path = Vendor; 195 | sourceTree = ""; 196 | }; 197 | /* End PBXGroup section */ 198 | 199 | /* Begin PBXNativeTarget section */ 200 | CE0E362A2212898700877823 /* YMC */ = { 201 | isa = PBXNativeTarget; 202 | buildConfigurationList = CE0E363B2212898A00877823 /* Build configuration list for PBXNativeTarget "YMC" */; 203 | buildPhases = ( 204 | CE0E36272212898700877823 /* Sources */, 205 | CE0E36282212898700877823 /* Frameworks */, 206 | CE0E36292212898700877823 /* Resources */, 207 | CECAAF7F221BD47900D17EC5 /* Embed Frameworks */, 208 | CECAAF86221BD5A400D17EC5 /* CopyFiles */, 209 | ); 210 | buildRules = ( 211 | ); 212 | dependencies = ( 213 | ); 214 | name = YMC; 215 | productName = YMC; 216 | productReference = CE0E362B2212898700877823 /* YMC.app */; 217 | productType = "com.apple.product-type.application"; 218 | }; 219 | /* End PBXNativeTarget section */ 220 | 221 | /* Begin PBXProject section */ 222 | CE0E36232212898700877823 /* Project object */ = { 223 | isa = PBXProject; 224 | attributes = { 225 | LastSwiftUpdateCheck = 1010; 226 | LastUpgradeCheck = 1010; 227 | ORGANIZATIONNAME = "Maksim Karelov"; 228 | TargetAttributes = { 229 | CE0E362A2212898700877823 = { 230 | CreatedOnToolsVersion = 10.1; 231 | LastSwiftMigration = 1020; 232 | SystemCapabilities = { 233 | com.apple.HardenedRuntime = { 234 | enabled = 0; 235 | }; 236 | com.apple.Sandbox = { 237 | enabled = 0; 238 | }; 239 | }; 240 | }; 241 | }; 242 | }; 243 | buildConfigurationList = CE0E36262212898700877823 /* Build configuration list for PBXProject "YMC" */; 244 | compatibilityVersion = "Xcode 9.3"; 245 | developmentRegion = en; 246 | hasScannedForEncodings = 0; 247 | knownRegions = ( 248 | en, 249 | Base, 250 | ); 251 | mainGroup = CE0E36222212898700877823; 252 | productRefGroup = CE0E362C2212898700877823 /* Products */; 253 | projectDirPath = ""; 254 | projectRoot = ""; 255 | targets = ( 256 | CE0E362A2212898700877823 /* YMC */, 257 | ); 258 | }; 259 | /* End PBXProject section */ 260 | 261 | /* Begin PBXResourcesBuildPhase section */ 262 | CE0E36292212898700877823 /* Resources */ = { 263 | isa = PBXResourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | CE0E36332212898A00877823 /* Assets.xcassets in Resources */, 267 | CE0E36362212898A00877823 /* Main.storyboard in Resources */, 268 | ); 269 | runOnlyForDeploymentPostprocessing = 0; 270 | }; 271 | /* End PBXResourcesBuildPhase section */ 272 | 273 | /* Begin PBXSourcesBuildPhase section */ 274 | CE0E36272212898700877823 /* Sources */ = { 275 | isa = PBXSourcesBuildPhase; 276 | buildActionMask = 2147483647; 277 | files = ( 278 | CE89AC772213EEC20028760B /* Messaging.swift in Sources */, 279 | CE8623A4221BD1D400AEEF59 /* BinUtils.swift in Sources */, 280 | CEDB4D2C221D1F2E0012D50D /* Manifest.swift in Sources */, 281 | CE19E05D222270430047B234 /* AppDelegate+Messaging.swift in Sources */, 282 | CE0E362F2212898700877823 /* AppDelegate.swift in Sources */, 283 | CEC02744221E8A1A007B32EE /* Logger.swift in Sources */, 284 | CE19E05B22226FDF0047B234 /* AppDelegate+Notifications.swift in Sources */, 285 | CE19E05922226FA60047B234 /* AppDelegate+Permissions.swift in Sources */, 286 | CE19E05722226F2B0047B234 /* AppDelegate+MediaKeyTap.swift in Sources */, 287 | CE8759722217390A008C8B5C /* Player.swift in Sources */, 288 | CEE809D922128B3200686E75 /* PlayerViewController.swift in Sources */, 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | }; 292 | /* End PBXSourcesBuildPhase section */ 293 | 294 | /* Begin PBXVariantGroup section */ 295 | CE0E36342212898A00877823 /* Main.storyboard */ = { 296 | isa = PBXVariantGroup; 297 | children = ( 298 | CE0E36352212898A00877823 /* Base */, 299 | ); 300 | name = Main.storyboard; 301 | sourceTree = ""; 302 | }; 303 | /* End PBXVariantGroup section */ 304 | 305 | /* Begin XCBuildConfiguration section */ 306 | CE0E36392212898A00877823 /* Debug */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ALWAYS_SEARCH_USER_PATHS = NO; 310 | CLANG_ANALYZER_NONNULL = YES; 311 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_ENABLE_OBJC_WEAK = YES; 317 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 318 | CLANG_WARN_BOOL_CONVERSION = YES; 319 | CLANG_WARN_COMMA = YES; 320 | CLANG_WARN_CONSTANT_CONVERSION = YES; 321 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 322 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 323 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 324 | CLANG_WARN_EMPTY_BODY = YES; 325 | CLANG_WARN_ENUM_CONVERSION = YES; 326 | CLANG_WARN_INFINITE_RECURSION = YES; 327 | CLANG_WARN_INT_CONVERSION = YES; 328 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 330 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 331 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 332 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 333 | CLANG_WARN_STRICT_PROTOTYPES = YES; 334 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 335 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 336 | CLANG_WARN_UNREACHABLE_CODE = YES; 337 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 338 | CODE_SIGN_IDENTITY = "Mac Developer"; 339 | COPY_PHASE_STRIP = NO; 340 | DEBUG_INFORMATION_FORMAT = dwarf; 341 | ENABLE_STRICT_OBJC_MSGSEND = YES; 342 | ENABLE_TESTABILITY = YES; 343 | GCC_C_LANGUAGE_STANDARD = gnu11; 344 | GCC_DYNAMIC_NO_PIC = NO; 345 | GCC_NO_COMMON_BLOCKS = YES; 346 | GCC_OPTIMIZATION_LEVEL = 0; 347 | GCC_PREPROCESSOR_DEFINITIONS = ( 348 | "DEBUG=1", 349 | "$(inherited)", 350 | ); 351 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 352 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 353 | GCC_WARN_UNDECLARED_SELECTOR = YES; 354 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 355 | GCC_WARN_UNUSED_FUNCTION = YES; 356 | GCC_WARN_UNUSED_VARIABLE = YES; 357 | MACOSX_DEPLOYMENT_TARGET = 10.11; 358 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 359 | MTL_FAST_MATH = YES; 360 | ONLY_ACTIVE_ARCH = YES; 361 | SDKROOT = macosx; 362 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 363 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 364 | }; 365 | name = Debug; 366 | }; 367 | CE0E363A2212898A00877823 /* Release */ = { 368 | isa = XCBuildConfiguration; 369 | buildSettings = { 370 | ALWAYS_SEARCH_USER_PATHS = NO; 371 | CLANG_ANALYZER_NONNULL = YES; 372 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 373 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 374 | CLANG_CXX_LIBRARY = "libc++"; 375 | CLANG_ENABLE_MODULES = YES; 376 | CLANG_ENABLE_OBJC_ARC = YES; 377 | CLANG_ENABLE_OBJC_WEAK = YES; 378 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 379 | CLANG_WARN_BOOL_CONVERSION = YES; 380 | CLANG_WARN_COMMA = YES; 381 | CLANG_WARN_CONSTANT_CONVERSION = YES; 382 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 383 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 384 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 385 | CLANG_WARN_EMPTY_BODY = YES; 386 | CLANG_WARN_ENUM_CONVERSION = YES; 387 | CLANG_WARN_INFINITE_RECURSION = YES; 388 | CLANG_WARN_INT_CONVERSION = YES; 389 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 390 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 391 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 392 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 393 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 394 | CLANG_WARN_STRICT_PROTOTYPES = YES; 395 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 396 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 397 | CLANG_WARN_UNREACHABLE_CODE = YES; 398 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 399 | CODE_SIGN_IDENTITY = "Mac Developer"; 400 | COPY_PHASE_STRIP = NO; 401 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 402 | ENABLE_NS_ASSERTIONS = NO; 403 | ENABLE_STRICT_OBJC_MSGSEND = YES; 404 | GCC_C_LANGUAGE_STANDARD = gnu11; 405 | GCC_NO_COMMON_BLOCKS = YES; 406 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 407 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 408 | GCC_WARN_UNDECLARED_SELECTOR = YES; 409 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 410 | GCC_WARN_UNUSED_FUNCTION = YES; 411 | GCC_WARN_UNUSED_VARIABLE = YES; 412 | MACOSX_DEPLOYMENT_TARGET = 10.11; 413 | MTL_ENABLE_DEBUG_INFO = NO; 414 | MTL_FAST_MATH = YES; 415 | SDKROOT = macosx; 416 | SWIFT_COMPILATION_MODE = wholemodule; 417 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 418 | }; 419 | name = Release; 420 | }; 421 | CE0E363C2212898A00877823 /* Debug */ = { 422 | isa = XCBuildConfiguration; 423 | buildSettings = { 424 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 425 | CODE_SIGN_STYLE = Automatic; 426 | COMBINE_HIDPI_IMAGES = YES; 427 | DEVELOPMENT_TEAM = 4TH28AB8A3; 428 | FRAMEWORK_SEARCH_PATHS = ( 429 | "$(inherited)", 430 | "$(PROJECT_DIR)/Carthage/Build/Mac", 431 | ); 432 | INFOPLIST_FILE = YMC/Info.plist; 433 | LD_RUNPATH_SEARCH_PATHS = ( 434 | "$(inherited)", 435 | "@executable_path/../Frameworks", 436 | ); 437 | PRODUCT_BUNDLE_IDENTIFIER = info.karelov.YMC; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | SWIFT_VERSION = 5.0; 440 | }; 441 | name = Debug; 442 | }; 443 | CE0E363D2212898A00877823 /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 447 | CODE_SIGN_STYLE = Automatic; 448 | COMBINE_HIDPI_IMAGES = YES; 449 | DEVELOPMENT_TEAM = 4TH28AB8A3; 450 | FRAMEWORK_SEARCH_PATHS = ( 451 | "$(inherited)", 452 | "$(PROJECT_DIR)/Carthage/Build/Mac", 453 | ); 454 | INFOPLIST_FILE = YMC/Info.plist; 455 | LD_RUNPATH_SEARCH_PATHS = ( 456 | "$(inherited)", 457 | "@executable_path/../Frameworks", 458 | ); 459 | PRODUCT_BUNDLE_IDENTIFIER = info.karelov.YMC; 460 | PRODUCT_NAME = "$(TARGET_NAME)"; 461 | SWIFT_VERSION = 5.0; 462 | }; 463 | name = Release; 464 | }; 465 | /* End XCBuildConfiguration section */ 466 | 467 | /* Begin XCConfigurationList section */ 468 | CE0E36262212898700877823 /* Build configuration list for PBXProject "YMC" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | CE0E36392212898A00877823 /* Debug */, 472 | CE0E363A2212898A00877823 /* Release */, 473 | ); 474 | defaultConfigurationIsVisible = 0; 475 | defaultConfigurationName = Release; 476 | }; 477 | CE0E363B2212898A00877823 /* Build configuration list for PBXNativeTarget "YMC" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | CE0E363C2212898A00877823 /* Debug */, 481 | CE0E363D2212898A00877823 /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | /* End XCConfigurationList section */ 487 | }; 488 | rootObject = CE0E36232212898700877823 /* Project object */; 489 | } 490 | -------------------------------------------------------------------------------- /native/YMC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /native/YMC.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /native/YMC/Application/AppDelegate/AppDelegate+MediaKeyTap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+MediaKeyTap.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 24/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import MediaKeyTap 10 | 11 | extension AppDelegate: MediaKeyTapDelegate { 12 | func handle(mediaKey: MediaKey, event: KeyEvent) { 13 | switch mediaKey { 14 | case .playPause: 15 | if event.modifierFlags.contains(.command) { 16 | Player.instance.emitPressedButton(.TRACK_INFO) 17 | } else { 18 | Player.instance.emitPressedButton(.PLAY_PAUSE) 19 | } 20 | case .fastForward: 21 | if event.modifierFlags.contains(.command) { 22 | Player.instance.emitPressedButton(.LIKE) 23 | } else { 24 | Player.instance.emitPressedButton(.NEXT) 25 | } 26 | case .rewind: 27 | if event.modifierFlags.contains(.command) { 28 | Player.instance.emitPressedButton(.LINK) 29 | } else { 30 | Player.instance.emitPressedButton(.PREV) 31 | } 32 | default: 33 | return 34 | } 35 | } 36 | 37 | func readKeys() { 38 | if !isAccessibilityAvailable { return } 39 | 40 | mediaKeyTap = MediaKeyTap(delegate: self) 41 | mediaKeyTap?.start() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /native/YMC/Application/AppDelegate/AppDelegate+Messaging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+Messaging.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 24/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension AppDelegate { 12 | func setupIncomingMessage() { 13 | IncomingMessage.register(CurrentTrack.self, for: "TRACK_INFO") 14 | IncomingMessage.register(CurrentTrack.self, for: "CURRENT_TRACK") 15 | IncomingMessage.register(PlayerState.self, for: "PLAYER_STATE") 16 | IncomingMessage.register(Controls.self, for: "CONTROLS") 17 | IncomingMessage.register(IsPlaying.self, for: "PLAYING") 18 | IncomingMessage.register(IsLiked.self, for: "TOGGLE_LIKE") 19 | } 20 | 21 | func setupRefreshInterval() { 22 | Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(refreshInterval(_:)), userInfo: nil, repeats: true) 23 | } 24 | 25 | @objc func refreshInterval(_ timer: Timer) { 26 | sendMessageToFrontend("REFRESH") 27 | } 28 | 29 | func readMessages() { 30 | standardInput.readabilityHandler = { pipe in 31 | let data = pipe.availableData 32 | 33 | if data.count == 0 || data.count == 4 { return } 34 | 35 | guard let unpacked = try? unpack("=I", data.subdata(in: 0..<4)) else { return } 36 | guard let length = unpacked[0] as? Int else { return } 37 | 38 | var message: String 39 | 40 | if length > 4096 { 41 | message = String(data: data, encoding: .utf8) ?? "" 42 | } else { 43 | message = String(data: data.subdata(in: 4..<(length + 4)), encoding: .utf8) ?? "" 44 | } 45 | 46 | if message.count == 0 { return } 47 | 48 | self.handleMessage(message.data(using: .utf8)!) 49 | } 50 | } 51 | 52 | func handleMessage(_ message: Data) { 53 | guard let json = try? JSONDecoder().decode(IncomingMessage.self, from: message) else { return } 54 | 55 | Logger.instance.log(logLevel: .info, message: "MESSAGE FROM EXT: \(json)") 56 | 57 | switch json.data { 58 | case let data as PlayerState: 59 | Player.instance.changeCoverImage(data.currentTrack.cover) 60 | Player.instance.changeCurrentTrack(data.currentTrack) 61 | Player.instance.changeControls(data.controls) 62 | Player.instance.changePlayingState(data.isPlaying) 63 | case let data as CurrentTrack: 64 | Player.instance.changeCoverImage(data.cover) 65 | Player.instance.changeCurrentTrack(data) 66 | case let data as Controls: 67 | Player.instance.changeControls(data) 68 | case let data as IsPlaying: 69 | Player.instance.changePlayingState(data.state) 70 | case let data as IsLiked: 71 | Player.instance.changeLikedState(data.state) 72 | default: 73 | return 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /native/YMC/Application/AppDelegate/AppDelegate+Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+Notifications.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 24/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension AppDelegate { 13 | func showTrackInfoNotification() { 14 | guard let currentTrack = Player.instance.currentTrack else { return } 15 | let notification = NSUserNotification() 16 | 17 | notification.title = currentTrack.artist 18 | notification.informativeText = currentTrack.title 19 | notification.contentImage = Player.instance.coverImage 20 | 21 | NSUserNotificationCenter.default.deliver(notification) 22 | } 23 | 24 | func copyTrackLinkToClipboard() { 25 | if let link = Player.instance.currentTrack?.link?.absoluteString { 26 | let clipboard = NSPasteboard.general 27 | clipboard.clearContents() 28 | clipboard.setString(link, forType: .string) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /native/YMC/Application/AppDelegate/AppDelegate+Permissions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+Setup.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 24/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension AppDelegate { 13 | func prepareLaunch() { 14 | let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as String: true] as CFDictionary 15 | 16 | if !AXIsProcessTrustedWithOptions(options) { 17 | showAlert( 18 | title: "Action required", 19 | message: "To be able to switch songs by pressing media keys you must activate the app in accessibility settings", 20 | quitAfter: false 21 | ) 22 | } else { 23 | isAccessibilityAvailable = true 24 | } 25 | 26 | let writeManifestStatus = processManifests() 27 | 28 | switch writeManifestStatus { 29 | case .error: 30 | showAlert( 31 | title: "Error", 32 | message: "Cannot write manifest file for extension.\nApp will be closed.", 33 | quitAfter: true 34 | ) 35 | case .updated: 36 | showAlert( 37 | title: "Action required", 38 | message: "Manifest updated. Please, restart your browser.", 39 | quitAfter: true 40 | ) 41 | default: 42 | return 43 | } 44 | } 45 | 46 | private func showAlert(title: String, message: String, quitAfter: Bool) { 47 | let alert = NSAlert() 48 | 49 | alert.messageText = title 50 | alert.informativeText = message 51 | alert.addButton(withTitle: "Got it") 52 | 53 | if quitAfter && alert.runModal() == .alertFirstButtonReturn { 54 | exit(1) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /native/YMC/Application/AppDelegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 12/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import MediaKeyTap 11 | import RxSwift 12 | 13 | @NSApplicationMain 14 | class AppDelegate: NSObject, NSApplicationDelegate { 15 | let standardInput = FileHandle.standardInput 16 | 17 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 18 | let popover = NSPopover() 19 | 20 | let runStdinMonitoring = CommandLine.arguments.first(where: { 21 | $0.contains("ymc.json") || $0.contains("chrome-extension://") 22 | }) != nil 23 | var playerViewController: PlayerViewController? = nil 24 | var mediaKeyTap: MediaKeyTap? = nil 25 | var isAccessibilityAvailable = false 26 | 27 | let disposeBag = DisposeBag() 28 | 29 | func applicationDidFinishLaunching(_ aNotification: Notification) { 30 | prepareLaunch() 31 | 32 | playerViewController = PlayerViewController.freshController() 33 | 34 | setupIncomingMessage() 35 | setupStatusItem() 36 | setupPopover() 37 | setupRefreshInterval() 38 | readKeys() 39 | 40 | if runStdinMonitoring { 41 | readMessages() 42 | } 43 | 44 | Logger.instance.log(logLevel: .info, message: "runStdinMonitoring: \(runStdinMonitoring)") 45 | 46 | Player.instance.buttonPress$.subscribe { event in 47 | guard let button = event.element else { return } 48 | 49 | switch button { 50 | case .TRACK_INFO: 51 | self.showTrackInfoNotification() 52 | case .LINK: 53 | self.copyTrackLinkToClipboard() 54 | case .PLAY_PAUSE: 55 | self.sendMessageToFrontend("PLAY_PAUSE") 56 | case .LIKE: 57 | self.sendMessageToFrontend("TOGGLE_LIKE") 58 | case .NEXT: 59 | self.sendMessageToFrontend("NEXT") 60 | case .PREV: 61 | self.sendMessageToFrontend("PREV") 62 | case .REFRESH: 63 | self.sendMessageToFrontend("REFRESH") 64 | default: 65 | return 66 | } 67 | }.disposed(by: disposeBag) 68 | 69 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 70 | self.sendMessageToFrontend("REFRESH") 71 | } 72 | } 73 | 74 | func applicationWillTerminate(_ aNotification: Notification) { 75 | // Insert code here to tear down your application 76 | } 77 | 78 | func sendMessageToFrontend(_ messageType: String) { 79 | if runStdinMonitoring { 80 | writeMessage(messageType) 81 | } 82 | } 83 | 84 | @objc func togglePopover(_ sender: NSButton) { 85 | if popover.isShown { 86 | popover.performClose(sender) 87 | } else { 88 | if let button = statusItem.button { 89 | NSApplication.shared.activate(ignoringOtherApps: true) 90 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) 91 | } 92 | } 93 | } 94 | 95 | private func setupStatusItem(_ title: String? = nil) { 96 | if let button = statusItem.button { 97 | button.image = NSImage(named: "logo") 98 | button.action = #selector(togglePopover(_:)) 99 | button.sendAction(on: [.leftMouseDown, .rightMouseDown]) 100 | } 101 | } 102 | 103 | private func setupPopover() { 104 | popover.behavior = .transient 105 | popover.contentViewController = playerViewController 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /native/YMC/Application/PlayerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerViewController.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 12/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import RxSwift 11 | import SwiftyBeaver 12 | 13 | class PlayerViewController: NSViewController { 14 | 15 | @IBOutlet var songCover: NSImageView! 16 | @IBOutlet var songTitle: NSTextField! 17 | @IBOutlet var songArtist: NSTextField! 18 | 19 | @IBOutlet var rewindButton: NSButton! 20 | @IBOutlet var playPauseButton: NSButton! 21 | @IBOutlet var forwardButton: NSButton! 22 | 23 | @IBOutlet var linkButton: NSButton! 24 | @IBOutlet var likeButton: NSButton! 25 | @IBOutlet var settingsButton: NSButton! 26 | @IBOutlet var settingsPopupButton: NSPopUpButton! 27 | 28 | private let disposeBag = DisposeBag() 29 | 30 | private var invisibleWindow: NSWindow? = nil 31 | 32 | required init?(coder: NSCoder) { 33 | super.init(coder: coder) 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | linkButton.sendAction(on: .leftMouseDown) 40 | 41 | Player.instance.currentTrack$.subscribe { event in 42 | guard let currentTrack = event.element else { return } 43 | 44 | DispatchQueue.main.async { 45 | self.songTitle.stringValue = currentTrack.title 46 | self.songArtist.stringValue = currentTrack.artist 47 | self.likeButton.state = currentTrack.liked ? .on : .off 48 | } 49 | }.disposed(by: disposeBag) 50 | 51 | Player.instance.coverImage$.subscribe { event in 52 | guard let coverImage = event.element else { return } 53 | 54 | DispatchQueue.main.async { 55 | self.songCover.image = coverImage 56 | } 57 | }.disposed(by: disposeBag) 58 | 59 | Player.instance.playingState$.subscribe { event in 60 | guard let playingState = event.element else { return } 61 | let image = NSImage(named: (playingState ? "pause" : "play")) 62 | 63 | DispatchQueue.main.async { 64 | self.playPauseButton.image = image 65 | } 66 | }.disposed(by: disposeBag) 67 | 68 | Player.instance.availableControls$.subscribe { event in 69 | guard let controls = event.element else { return } 70 | 71 | DispatchQueue.main.async { 72 | self.likeButton.isEnabled = controls.like ?? false 73 | self.rewindButton.isEnabled = controls.prev ?? false 74 | self.forwardButton.isEnabled = controls.next ?? false 75 | } 76 | }.disposed(by: disposeBag) 77 | } 78 | 79 | @IBAction func buttonClick(_ sender: NSButton) { 80 | guard let identifier = sender.identifier?.rawValue else { return } 81 | 82 | switch identifier { 83 | case "play_pause": 84 | Player.instance.emitPressedButton(.PLAY_PAUSE) 85 | case "like": 86 | Player.instance.emitPressedButton(.LIKE) 87 | case "next": 88 | Player.instance.emitPressedButton(.NEXT) 89 | case "prev": 90 | Player.instance.emitPressedButton(.PREV) 91 | case "refresh": 92 | Player.instance.emitPressedButton(.REFRESH) 93 | default: 94 | return 95 | } 96 | } 97 | 98 | @IBAction func menuItemClick(_ sender: NSMenuItem) { 99 | guard let identifier = sender.identifier?.rawValue else { return } 100 | 101 | switch identifier { 102 | case "quit": 103 | exit(0) 104 | default: 105 | return 106 | } 107 | } 108 | 109 | 110 | @IBAction func settingsButtonClick(_ sender: NSButton) { 111 | settingsPopupButton.performClick(sender) 112 | } 113 | 114 | @IBAction func shareButtonClick(_ sender: NSButton) { 115 | guard let url = Player.instance.currentTrack?.link else { return } 116 | 117 | // Create a window 118 | invisibleWindow = NSWindow(contentRect: NSMakeRect(0, 0, 20, 5), styleMask: .borderless, backing: .buffered, defer: false) 119 | invisibleWindow!.backgroundColor = .red 120 | invisibleWindow!.alphaValue = 0 121 | 122 | // find the coordinates of the statusBarItem in screen space 123 | let buttonRect: NSRect = sender.convert(sender.bounds, to: nil) 124 | let screenRect: NSRect = sender.window!.convertToScreen(buttonRect) 125 | 126 | // calculate the bottom center position (10 is the half of the window width) 127 | let posX = screenRect.origin.x + (screenRect.width / 2) - 10 128 | let posY = screenRect.origin.y 129 | 130 | // position and show the window 131 | invisibleWindow!.setFrameOrigin(NSPoint(x: posX, y: posY)) 132 | invisibleWindow!.makeKeyAndOrderFront(self) 133 | 134 | guard let view = invisibleWindow!.contentView else { return } 135 | 136 | let picker = NSSharingServicePicker(items: [url.absoluteString]) 137 | 138 | picker.delegate = self 139 | picker.show(relativeTo: view.frame, of: view, preferredEdge: .minY) 140 | } 141 | } 142 | 143 | extension PlayerViewController { 144 | static func freshController() -> PlayerViewController { 145 | let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) 146 | let identifier = NSStoryboard.SceneIdentifier("PlayerViewController") 147 | 148 | guard let viewController = storyboard.instantiateController(withIdentifier: identifier) as? PlayerViewController else { 149 | fatalError("Cannot find PlayerViewController") 150 | } 151 | 152 | return viewController 153 | } 154 | } 155 | 156 | extension PlayerViewController: NSSharingServicePickerDelegate { 157 | func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) { 158 | invisibleWindow?.close() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/256-1.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/32-1.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/512-1.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "32-1.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "256-1.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "512-1.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/forward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "fast-forward.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "template", 15 | "preserves-vector-representation" : true 16 | } 17 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/forward.imageset/fast-forward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/forward.imageset/fast-forward.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/heart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "heart.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "template" 15 | } 16 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/heart.imageset/heart.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/heart.imageset/heart.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/link.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "share.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "template" 15 | } 16 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/link.imageset/share.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/link.imageset/share.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "original" 15 | } 16 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/logo.imageset/logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/logo.imageset/logo.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/logo_large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo_large.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/logo_large.imageset/logo_large.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/logo_large.imageset/logo_large.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pause.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "template", 15 | "preserves-vector-representation" : true 16 | } 17 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/pause.imageset/pause.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/pause.imageset/pause.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/play.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "play.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "template", 15 | "preserves-vector-representation" : true 16 | } 17 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/play.imageset/play.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/play.imageset/play.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/refresh.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "refresh.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "template" 15 | } 16 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/refresh.imageset/refresh.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/refresh.imageset/refresh.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/rewind.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rewind.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "template", 15 | "preserves-vector-representation" : true 16 | } 17 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/rewind.imageset/rewind.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/rewind.imageset/rewind.pdf -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "settings.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "compression-type" : "lossless", 14 | "template-rendering-intent" : "template" 15 | } 16 | } -------------------------------------------------------------------------------- /native/YMC/Assets.xcassets/settings.imageset/settings.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ty3uK/YMC/a4ae8f63b5a809f3b83860972f0bef49bf28118f/native/YMC/Assets.xcassets/settings.imageset/settings.pdf -------------------------------------------------------------------------------- /native/YMC/Classes/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 21/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyBeaver 11 | 12 | enum LogLevel { 13 | case info 14 | case error 15 | case warning 16 | } 17 | 18 | class Logger { 19 | static var instance: Logger { 20 | get { return _instance } 21 | } 22 | 23 | var enabled: Bool { 24 | get { return UserDefaults.standard.bool(forKey: "debug") } 25 | } 26 | 27 | private static let _instance = Logger() 28 | private var logger: SwiftyBeaver.Type 29 | 30 | private init() { 31 | logger = SwiftyBeaver.self 32 | 33 | let file = FileDestination() 34 | 35 | file.logFileURL = URL(fileURLWithPath: "/tmp/ymc.log") 36 | logger.addDestination(file) 37 | } 38 | 39 | func log(logLevel: LogLevel, message: Any) { 40 | if !enabled { return } 41 | 42 | switch logLevel { 43 | case .info: 44 | logger.info(message) 45 | case .warning: 46 | logger.warning(message) 47 | case .error: 48 | logger.error(message) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /native/YMC/Classes/Manifest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Manifest.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 20/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Files 11 | 12 | enum WriteManifestStatus { 13 | case updated 14 | case skipped 15 | case error 16 | } 17 | 18 | struct Manifest: Codable, Equatable { 19 | let name: String 20 | let description: String 21 | let path: String 22 | let type: String 23 | var allowed_extensions: [String]? = nil 24 | var allowed_origins: [String]? = nil 25 | 26 | init(name: String, description: String, path: String, type: String) { 27 | self.name = name 28 | self.description = description 29 | self.path = path 30 | self.type = type 31 | } 32 | 33 | static func == (a: Manifest, b: Manifest) -> Bool { 34 | return ( 35 | a.name == b.name && 36 | a.description == b.description && 37 | a.path == b.path && 38 | a.type == b.type && 39 | ( 40 | (a.allowed_extensions != nil && b.allowed_extensions != nil && a.allowed_extensions!.elementsEqual(b.allowed_extensions!)) || 41 | (a.allowed_origins != nil && b.allowed_origins != nil && a.allowed_origins!.elementsEqual(b.allowed_origins!)) 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | private let paths = [ 48 | "\(Folder.home.path)Library/Application Support/Mozilla", 49 | "\(Folder.home.path)Library/Application Support/Google/Chrome", 50 | "\(Folder.home.path)Library/Application Support/Chromium", 51 | "\(Folder.home.path)Library/Application Support/com.operasoftware.Opera", 52 | "\(Folder.home.path)Library/Application Support/Yandex/YandexBrowser", 53 | "\(Folder.home.path)Library/Application Support/Vivaldi" 54 | ] 55 | 56 | private func createManifest(appPath: String, isChromeBasedBrowser: Bool) -> Manifest { 57 | var manifest = Manifest( 58 | name: "ymc", 59 | description: "Control Yandex.Music from any window in MacOS", 60 | path: appPath, 61 | type: "stdio" 62 | ) 63 | 64 | if isChromeBasedBrowser { 65 | manifest.allowed_origins = ["chrome-extension://mhjachgjcpppedbkfaajhodfgiilbmci/"] 66 | } else { 67 | manifest.allowed_extensions = ["ymc@karelov.info"] 68 | } 69 | 70 | return manifest 71 | } 72 | 73 | private func writeManifest(_ path: String) -> WriteManifestStatus { 74 | guard let appPath = Bundle.main.executablePath else { return .error } 75 | 76 | let manifest = createManifest(appPath: appPath, isChromeBasedBrowser: !path.contains("Mozilla")) 77 | let encoder = JSONEncoder() 78 | 79 | encoder.outputFormatting = .prettyPrinted 80 | 81 | guard let manifestData = try? encoder.encode(manifest) else { return .error } 82 | guard let manifestDataString = String(data: manifestData, encoding: .utf8)? 83 | .replacingOccurrences(of: "\\/", with: "/") 84 | .replacingOccurrences(of: " : ", with: ": ") else { return .error } 85 | guard let manifestDataFixed = manifestDataString.data(using: .utf8) else { return .error } 86 | guard let browserFolder = try? Folder(path: path) else { return .skipped } 87 | 88 | var folder: Folder 89 | 90 | if let targetFolder = try? browserFolder.subfolder(named: "NativeMessagingHosts") { 91 | folder = targetFolder 92 | } else if let targetFolder = try? browserFolder.createSubfolder(named: "NativeMessagingHosts") { 93 | folder = targetFolder 94 | } else { 95 | return .skipped 96 | } 97 | 98 | var targetFile: File? 99 | 100 | if let file = try? folder.file(named: "ymc.json") { 101 | guard let existingManifestData = try? file.read() else { return .error } 102 | 103 | if let existingManifest = try? JSONDecoder().decode(Manifest.self, from: existingManifestData), existingManifest == manifest { 104 | return .skipped 105 | } 106 | 107 | targetFile = file 108 | } else { 109 | targetFile = try? folder.createFile(named: "ymc.json") 110 | } 111 | 112 | if targetFile == nil { 113 | return .error 114 | } 115 | 116 | do { 117 | try targetFile!.write(data: manifestDataFixed) 118 | } catch { 119 | return .error 120 | } 121 | 122 | return .updated 123 | } 124 | 125 | func processManifests() -> WriteManifestStatus { 126 | let results = paths.map { writeManifest($0) } 127 | 128 | if results.contains(.error) { 129 | return .error 130 | } else if results.contains(.updated) { 131 | return .updated 132 | } 133 | 134 | return .skipped 135 | } 136 | -------------------------------------------------------------------------------- /native/YMC/Classes/Messaging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Messaging.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 26/01/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct OutgoingMessage: Encodable { 12 | let type: String 13 | } 14 | 15 | struct EncodedOutgoingMessage { 16 | let content: String 17 | let length: String 18 | } 19 | 20 | struct IncomingMessage: Decodable { 21 | let type: String 22 | let data: Any? 23 | 24 | private enum CodingKeys: String, CodingKey { 25 | case type 26 | case data 27 | } 28 | 29 | private typealias IncomingMessageDataDecoder = (KeyedDecodingContainer) throws -> Any 30 | 31 | private static var decoders: [String: IncomingMessageDataDecoder] = [:] 32 | 33 | static func register(_ type: A.Type, for typeName: String) { 34 | decoders[typeName] = { container in 35 | try container.decode(A.self, forKey: .data) 36 | } 37 | } 38 | 39 | init(from decoder: Decoder) throws { 40 | let container = try decoder.container(keyedBy: CodingKeys.self) 41 | type = try container.decode(String.self, forKey: .type) 42 | 43 | if let decode = IncomingMessage.decoders[type] { 44 | data = try decode(container) 45 | } else { 46 | data = nil 47 | } 48 | } 49 | } 50 | 51 | extension FileHandle : TextOutputStream { 52 | public func write(_ string: String) { 53 | guard let data = string.data(using: .utf8) else { return } 54 | self.write(data) 55 | } 56 | } 57 | 58 | private func encodeMessage(_ message: OutgoingMessage) -> EncodedOutgoingMessage? { 59 | guard let json = try? JSONEncoder().encode(message), 60 | let jsonString = String(data: json, encoding: .utf8), 61 | let lengthString = String(data: pack("=I", [json.count], .utf8), encoding: .utf8) 62 | else { 63 | return nil 64 | } 65 | 66 | return EncodedOutgoingMessage( 67 | content: jsonString, 68 | length: lengthString 69 | ) 70 | } 71 | 72 | private func writeEncodedMessage(_ message: EncodedOutgoingMessage) { 73 | var standardOutput = FileHandle.standardOutput 74 | print(message.length, separator: "", terminator: "", to: &standardOutput) 75 | print(message.content, separator: "", terminator: "", to: &standardOutput) 76 | fflush(__stdoutp) 77 | } 78 | 79 | func writeMessage(_ messageType: String) { 80 | if let encodedMessage = encodeMessage(OutgoingMessage(type: messageType)) { 81 | writeEncodedMessage(encodedMessage) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /native/YMC/Classes/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // YMC 4 | // 5 | // Created by Максим Карелов on 15/02/2019. 6 | // Copyright © 2019 Maksim Karelov. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | import RxSwift 12 | 13 | enum PlayerControlsAvailability { 14 | case PREV 15 | case NEXT 16 | case LIKE 17 | } 18 | 19 | enum PlayerButtons { 20 | case PREV 21 | case PLAY_PAUSE 22 | case NEXT 23 | case LINK 24 | case LIKE 25 | case TRACK_INFO 26 | case REFRESH 27 | case NOOP 28 | } 29 | 30 | struct CurrentTrack: Decodable { 31 | let title: String 32 | let artist: String 33 | let cover: URL? 34 | let liked: Bool 35 | let link: URL? 36 | } 37 | 38 | struct Controls: Decodable { 39 | let next: Bool? 40 | let prev: Bool? 41 | let like: Bool? 42 | } 43 | 44 | struct IsPlaying: Decodable { 45 | let state: Bool 46 | } 47 | 48 | struct IsLiked: Decodable { 49 | let state: Bool 50 | } 51 | 52 | struct PlayerState: Decodable { 53 | let currentTrack: CurrentTrack 54 | let controls: Controls 55 | let isPlaying: Bool 56 | } 57 | 58 | class Player { 59 | private static let _instance = Player() 60 | 61 | static var instance: Player { 62 | get { return _instance } 63 | } 64 | 65 | private let _availableControls$ = BehaviorSubject(value: Controls( 66 | next: true, 67 | prev: false, 68 | like: false 69 | )) 70 | private let _playingState$ = BehaviorSubject(value: false) 71 | private let _currentTrack$ = BehaviorSubject(value: CurrentTrack( 72 | title: "No title", 73 | artist: "No artist", 74 | cover: nil, 75 | liked: false, 76 | link: nil 77 | )) 78 | private let _buttonPress$ = BehaviorSubject(value: PlayerButtons.NOOP) 79 | private let _coverImage$ = BehaviorSubject(value: NSImage(named: "logo_large")) 80 | 81 | public var availableControls$: Observable 82 | public var playingState$: Observable 83 | public var currentTrack$: Observable 84 | public var buttonPress$: Observable 85 | public var coverImage$: Observable 86 | 87 | public var currentTrack: CurrentTrack? { 88 | get { 89 | guard let result = try? _currentTrack$.value() else { return nil } 90 | return result 91 | } 92 | } 93 | public var coverImage: NSImage? { 94 | get { 95 | guard let result = ((try? _coverImage$.value()) as NSImage??) else { return nil } 96 | return result 97 | } 98 | } 99 | 100 | private init() { 101 | availableControls$ = _availableControls$.asObservable() 102 | playingState$ = _playingState$.asObservable() 103 | currentTrack$ = _currentTrack$.asObservable() 104 | buttonPress$ = _buttonPress$.asObservable() 105 | coverImage$ = _coverImage$.asObservable() 106 | } 107 | 108 | public func changeControls(_ controls: Controls) { 109 | _availableControls$.onNext(controls) 110 | } 111 | 112 | public func changePlayingState(_ state: Bool) { 113 | _playingState$.onNext(state) 114 | } 115 | 116 | public func changeCurrentTrack(_ state: CurrentTrack) { 117 | _currentTrack$.onNext(state) 118 | } 119 | 120 | public func changeLikedState(_ state: Bool) { 121 | guard let currentTrack = try? _currentTrack$.value() else { return } 122 | 123 | let newCurrentTrack = CurrentTrack( 124 | title: currentTrack.title, 125 | artist: currentTrack.artist, 126 | cover: currentTrack.cover, 127 | liked: state, 128 | link: currentTrack.link 129 | ) 130 | 131 | _currentTrack$.onNext(newCurrentTrack) 132 | } 133 | 134 | public func emitPressedButton(_ button: PlayerButtons) { 135 | _buttonPress$.onNext(button) 136 | } 137 | 138 | public func changeCoverImage(_ url: URL?) { 139 | guard let url = url else { 140 | _coverImage$.onNext(NSImage(named: "logo_large")) 141 | return 142 | } 143 | 144 | URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in 145 | guard let data = data, error == nil else { return } 146 | self._coverImage$.onNext(NSImage(data: data)) 147 | }).resume() 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /native/YMC/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0.4 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleURLName 25 | info.karelov.YMC 26 | CFBundleURLSchemes 27 | 28 | ymc 29 | 30 | 31 | 32 | CFBundleVersion 33 | 5 34 | LSMinimumSystemVersion 35 | $(MACOSX_DEPLOYMENT_TARGET) 36 | LSUIElement 37 | 38 | NSAppTransportSecurity 39 | 40 | NSAllowsArbitraryLoads 41 | 42 | 43 | NSHumanReadableCopyright 44 | Copyright © 2019 Maksim Karelov. All rights reserved. 45 | NSMainStoryboardFile 46 | Main 47 | NSPrincipalClass 48 | NSApplication 49 | 50 | 51 | -------------------------------------------------------------------------------- /native/YMC/Vendor/BinUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BinUtils.swift 3 | // BinUtils 4 | // 5 | // Created by Nicolas Seriot on 12/03/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreFoundation 11 | 12 | // MARK: protocol UnpackedType 13 | 14 | public protocol Unpackable {} 15 | 16 | extension NSString: Unpackable {} 17 | extension Bool: Unpackable {} 18 | extension Int: Unpackable {} 19 | extension Double: Unpackable {} 20 | 21 | // MARK: protocol DataConvertible 22 | 23 | protocol DataConvertible {} 24 | 25 | extension DataConvertible { 26 | 27 | init?(data: Data) { 28 | guard data.count == MemoryLayout.size else { return nil } 29 | self = data.withUnsafeBytes { $0.pointee } 30 | } 31 | 32 | init?(bytes: [UInt8]) { 33 | let data = Data(bytes:bytes) 34 | self.init(data:data) 35 | } 36 | 37 | var data: Data { 38 | var value = self 39 | return Data(buffer: UnsafeBufferPointer(start: &value, count: 1)) 40 | } 41 | } 42 | 43 | extension Bool : DataConvertible { } 44 | 45 | extension Int8 : DataConvertible { } 46 | extension Int16 : DataConvertible { } 47 | extension Int32 : DataConvertible { } 48 | extension Int64 : DataConvertible { } 49 | 50 | extension UInt8 : DataConvertible { } 51 | extension UInt16 : DataConvertible { } 52 | extension UInt32 : DataConvertible { } 53 | extension UInt64 : DataConvertible { } 54 | 55 | extension Float32 : DataConvertible { } 56 | extension Float64 : DataConvertible { } 57 | 58 | // MARK: String extension 59 | 60 | extension String { 61 | subscript (from:Int, to:Int) -> String { 62 | return NSString(string: self).substring(with: NSMakeRange(from, to-from)) 63 | } 64 | } 65 | 66 | // MARK: Data extension 67 | 68 | extension Data { 69 | var bytes : [UInt8] { 70 | return self.withUnsafeBytes { 71 | [UInt8](UnsafeBufferPointer(start: $0, count: self.count)) 72 | } 73 | } 74 | } 75 | 76 | // MARK: functions 77 | 78 | public func hexlify(_ data:Data) -> String { 79 | 80 | // similar to hexlify() in Python's binascii module 81 | // https://docs.python.org/2/library/binascii.html 82 | 83 | var s = String() 84 | var byte: UInt8 = 0 85 | 86 | for i in 0 ..< data.count { 87 | NSData(data: data).getBytes(&byte, range: NSMakeRange(i, 1)) 88 | s = s.appendingFormat("%02x", byte) 89 | } 90 | 91 | return s as String 92 | } 93 | 94 | public func unhexlify(_ string:String) -> Data? { 95 | 96 | // similar to unhexlify() in Python's binascii module 97 | // https://docs.python.org/2/library/binascii.html 98 | 99 | let s = string.uppercased().replacingOccurrences(of: " ", with: "") 100 | 101 | let nonHexCharacterSet = CharacterSet(charactersIn: "0123456789ABCDEF").inverted 102 | if let range = s.rangeOfCharacter(from: nonHexCharacterSet) { 103 | print("-- found non hex character at range \(range)") 104 | return nil 105 | } 106 | 107 | var data = Data(capacity: s.count / 2) 108 | 109 | for i in stride(from: 0, to:s.count, by:2) { 110 | let byteString = s[i, i+2] 111 | let byte = UInt8(byteString.withCString { strtoul($0, nil, 16) }) 112 | data.append([byte] as [UInt8], count: 1) 113 | } 114 | 115 | return data 116 | } 117 | 118 | func readIntegerType(_ type:T.Type, bytes:[UInt8], loc:inout Int) -> T { 119 | let size = MemoryLayout.size 120 | let sub = Array(bytes[loc..<(loc+size)]) 121 | loc += size 122 | return T(bytes: sub)! 123 | } 124 | 125 | func readFloatingPointType(_ type:T.Type, bytes:[UInt8], loc:inout Int, isBigEndian:Bool) -> T { 126 | let size = MemoryLayout.size 127 | let sub = Array(bytes[loc..<(loc+size)]) 128 | loc += size 129 | let sub_ = isBigEndian ? sub.reversed() : sub 130 | return T(bytes: sub_)! 131 | } 132 | 133 | func isBigEndianFromMandatoryByteOrderFirstCharacter(_ format:String) -> Bool { 134 | 135 | guard let firstChar = format.first else { assertionFailure("empty format"); return false } 136 | 137 | let s = NSString(string: String(firstChar)) 138 | let c = s.substring(to: 1) 139 | 140 | if c == "@" { assertionFailure("native size and alignment is unsupported") } 141 | 142 | if c == "=" || c == "<" { return false } 143 | if c == ">" || c == "!" { return true } 144 | 145 | assertionFailure("format '\(format)' first character must be among '=<>!'") 146 | 147 | return false 148 | } 149 | 150 | // akin to struct.calcsize(fmt) 151 | func numberOfBytesInFormat(_ format:String) -> Int { 152 | 153 | var numberOfBytes = 0 154 | 155 | var n = 0 // repeat counter 156 | 157 | var mutableFormat = format 158 | 159 | while !mutableFormat.isEmpty { 160 | 161 | let c = mutableFormat.remove(at: mutableFormat.startIndex) 162 | 163 | if let i = Int(String(c)) , 0...9 ~= i { 164 | if n > 0 { n *= 10 } 165 | n += i 166 | continue 167 | } 168 | 169 | if c == "s" { 170 | numberOfBytes += max(n,1) 171 | n = 0 172 | continue 173 | } 174 | 175 | let repeatCount = max(n,1) 176 | 177 | switch(c) { 178 | 179 | case "@", "<", "=", ">", "!", " ": 180 | () 181 | case "c", "b", "B", "x", "?": 182 | numberOfBytes += 1 * repeatCount 183 | case "h", "H": 184 | numberOfBytes += 2 * repeatCount 185 | case "i", "l", "I", "L", "f": 186 | numberOfBytes += 4 * repeatCount 187 | case "q", "Q", "d": 188 | numberOfBytes += 8 * repeatCount 189 | case "P": 190 | numberOfBytes += MemoryLayout.size * repeatCount 191 | default: 192 | assertionFailure("-- unsupported format \(c)") 193 | } 194 | 195 | n = 0 196 | } 197 | 198 | return numberOfBytes 199 | } 200 | 201 | func formatDoesMatchDataLength(_ format:String, data:Data) -> Bool { 202 | let sizeAccordingToFormat = numberOfBytesInFormat(format) 203 | let dataLength = data.count 204 | if sizeAccordingToFormat != dataLength { 205 | print("format \"\(format)\" expects \(sizeAccordingToFormat) bytes but data is \(dataLength) bytes") 206 | return false 207 | } 208 | 209 | return true 210 | } 211 | 212 | /* 213 | pack() and unpack() should behave as Python's struct module https://docs.python.org/2/library/struct.html BUT: 214 | - native size and alignment '@' is not supported 215 | - as a consequence, the byte order specifier character is mandatory and must be among "=<>!" 216 | - native byte order '=' assumes a little-endian system (eg. Intel x86) 217 | - Pascal strings 'p' and native pointers 'P' are not supported 218 | */ 219 | 220 | public enum BinUtilsError: Error { 221 | case formatDoesMatchDataLength(format:String, dataSize:Int) 222 | case unsupportedFormat(character:Character) 223 | } 224 | 225 | public func pack(_ format:String, _ objects:[Any], _ stringEncoding:String.Encoding=String.Encoding.windowsCP1252) -> Data { 226 | 227 | var objectsQueue = objects 228 | 229 | var mutableFormat = format 230 | 231 | var mutableData = Data() 232 | 233 | var isBigEndian = false 234 | 235 | let firstCharacter = mutableFormat.remove(at: mutableFormat.startIndex) 236 | 237 | switch(firstCharacter) { 238 | case "<", "=": 239 | isBigEndian = false 240 | case ">", "!": 241 | isBigEndian = true 242 | case "@": 243 | assertionFailure("native size and alignment '@' is unsupported'") 244 | default: 245 | assertionFailure("unsupported format chacracter'") 246 | } 247 | 248 | var n = 0 // repeat counter 249 | 250 | while !mutableFormat.isEmpty { 251 | 252 | let c = mutableFormat.remove(at: mutableFormat.startIndex) 253 | 254 | if let i = Int(String(c)) , 0...9 ~= i { 255 | if n > 0 { n *= 10 } 256 | n += i 257 | continue 258 | } 259 | 260 | var o : Any = 0 261 | 262 | if c == "s" { 263 | o = objectsQueue.remove(at: 0) 264 | 265 | guard let stringData = (o as! String).data(using: .utf8) else { assertionFailure(); return Data() } 266 | var bytes = stringData.bytes 267 | 268 | let expectedSize = max(1, n) 269 | 270 | // pad ... 271 | while bytes.count < expectedSize { bytes.append(0x00) } 272 | 273 | // ... or trunk 274 | if bytes.count > expectedSize { bytes = Array(bytes[0.. [Unpackable] { 340 | 341 | assert(CFByteOrderGetCurrent() == 1 /* CFByteOrderLittleEndian */, "\(#file) assumes little endian, but host is big endian") 342 | 343 | let isBigEndian = isBigEndianFromMandatoryByteOrderFirstCharacter(format) 344 | 345 | if formatDoesMatchDataLength(format, data: data) == false { 346 | throw BinUtilsError.formatDoesMatchDataLength(format:format, dataSize:data.count) 347 | } 348 | 349 | var a : [Unpackable] = [] 350 | 351 | var loc = 0 352 | 353 | let bytes = data.bytes 354 | 355 | var n = 0 // repeat counter 356 | 357 | var mutableFormat = format 358 | 359 | mutableFormat.remove(at: mutableFormat.startIndex) // consume byte-order specifier 360 | 361 | while !mutableFormat.isEmpty { 362 | 363 | let c = mutableFormat.remove(at: mutableFormat.startIndex) 364 | 365 | if let i = Int(String(c)) , 0...9 ~= i { 366 | if n > 0 { n *= 10 } 367 | n += i 368 | continue 369 | } 370 | 371 | if c == "s" { 372 | let length = max(n,1) 373 | let sub = Array(bytes[loc.. 2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------