Compare commits

...

210 Commits

Author SHA1 Message Date
Koen 3f22c7f717 Added polycentric user agent. 2024-04-24 16:02:26 +02:00
Kelvin f36e9588cb Use proper calls for thumbnail 2024-04-23 21:15:18 +02:00
Kelvin 8f99f399ee Refs and tests 2024-04-23 19:26:30 +02:00
Kelvin 56166a7948 Support for chinese, japanese, arabic file names for export 2024-04-23 19:24:59 +02:00
Kelvin 4edd8ee1ea Fix crash on extreme pip aspect ratios 2024-04-23 17:31:10 +02:00
Koen a830c918ab Finished embedding bilibili. 2024-04-23 15:07:10 +02:00
Kelvin 53f74c4b6e Fix for hanging app if logging is enabled 2024-04-22 19:58:22 +02:00
Kelvin 959c192762 Fix channel content not showing older non-videos, fix seperated channel contents not being fetched if not both streams and videos are included 2024-04-19 22:40:13 +02:00
Kelvin 8be7b1272b PeekChannelContent and initial algorithm support, DevSubmit support, Prevent crash url search before init, Documentation url scanning for devportal, limit ongoing downloads ui to 4 2024-04-19 20:16:09 +02:00
Kelvin 6b57878275 Fix post detail loader not disappearing 2024-04-16 21:15:47 +02:00
Kelvin 66c7741c38 Deleting playlist video deletes local files, Post links are now clickable, going back to channel page from post now shows channel page correctly, search capabilities correct for channel content search, Fix loader not disappearing in certain cases on post details 2024-04-16 21:15:28 +02:00
Kelvin b370af9d91 Grayjay logo, WatchLater button, WatchLater download, Download notification dismiss fix, Polycentric open platform, Minor utility additions, Dev method documentation url support 2024-04-12 23:39:33 +02:00
Kelvin 40b86cb5de Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-03-19 20:41:56 +01:00
Kelvin 84622e22aa Logo replacement 2024-03-19 20:41:01 +01:00
Koen 092b20041e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-03-08 08:17:51 +01:00
Koen f6cc00f471 Casting. 2024-03-08 08:17:38 +01:00
Kelvin be2067067b Year rounding 2024-03-06 21:59:55 +01:00
Kelvin 67a7dd9698 Refs 2024-03-06 21:44:48 +01:00
Kelvin 6ffc067b24 Support for cache in reconstructions, non-required cache added to exports, playlists shares now add a cache aswell for quicker importing 2024-03-06 21:39:30 +01:00
Kelvin 56e6314c11 Ref 2024-03-05 17:15:36 +01:00
Kelvin e590bb4a19 Fix 0 year issue 2024-03-05 00:05:46 +01:00
Kelvin 35fe7f0e7a Add type to unknown content exception 2024-03-01 15:31:30 +01:00
Koen 45d818ac81 Reverted dependencies. 2024-02-16 15:51:59 +01:00
Kelvin 7729681829 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-02-16 14:58:28 +01:00
Kelvin b12d04b27d Attempted fix for double controls 2024-02-16 14:58:17 +01:00
Koen e6608b9a5c Updated PolycentricAndroid. 2024-02-16 14:07:27 +01:00
Koen 2d503dfaf6 Added scroll to top. Full scrollable parent comment and Polycentric process secret backup and automatic database recovery. 2024-02-16 13:56:14 +01:00
Kelvin 08934ef8de Modify subscription groups in sub settings 2024-02-14 23:25:58 +01:00
Kelvin 62d927739a Sharing from overview options, notification channel names 2024-02-14 20:15:12 +01:00
Kelvin c8db8f58e8 Refs 2024-02-14 19:19:24 +01:00
Kelvin 0fc966a77d Subscription watched filter 2024-02-14 19:18:35 +01:00
Kelvin 9f6c6c8cf3 Fix support, fix membership urls 2024-01-23 23:51:21 +01:00
Kelvin 43a6ff138c Fix queue looping 2024-01-22 20:54:40 +01:00
Kelvin 269a3460e7 Fix live stream retrying 2024-01-22 15:52:51 +01:00
Koen 18150e9e15 Fixed bottom menu button visibility. 2024-01-19 20:29:00 +01:00
Koen 362c7f5b2c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-19 20:24:35 +01:00
Koen 2adb8ad7f9 Fixed brightness not working. 2024-01-19 20:24:23 +01:00
Kelvin 6b5d4e7507 Fix nullable 2024-01-19 19:44:52 +01:00
Kelvin 49c82726f0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-19 17:28:45 +01:00
Kelvin c8ddcda384 Refs, Dev portal improvements and on-device testing, Fix crashes on disabling v8 race conditions, edgecase where history could be null, issue on starting Grayjay with an url 2024-01-19 17:28:35 +01:00
Koen b75217f789 Possible fix for AudioNoisyReceiver popping up 'App is not responding'. 2024-01-19 17:02:24 +01:00
Koen 8ba8e535bd Added check for updates button on exception activity. 2024-01-19 16:13:42 +01:00
Koen e4c574db6b Fixed crash in updateAllButtonVisibility. 2024-01-19 15:38:22 +01:00
Koen fae73293d7 Fixed crash where it would fail to unregister audio noisy receiver. Fixed crash where system brightness setting does not exist. 2024-01-19 15:17:11 +01:00
Koen 3bd0aac4f8 Implemented system brightness in an alternative way. 2024-01-19 14:09:56 +01:00
Kelvin 26b822e04b Text edit 2024-01-17 17:23:38 +01:00
Kelvin 96b9b8843c Fix wrong visibility for no sources ui 2024-01-17 16:57:35 +01:00
Kelvin 6d9c1e17b5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 15:31:11 +01:00
Kelvin 507ad105c0 Hide warnings if empty, enable newly installed plugins, new browse plugin url 2024-01-17 15:31:05 +01:00
Koen 40a283017e Fixed issue where adding new playlist would require two back swipes to minimize video. 2024-01-17 15:26:09 +01:00
Kelvin be14597670 Merge 2024-01-17 13:28:54 +01:00
Kelvin 837609abb9 Remove primary client, remove play store default source, add additional flows for adding sources 2024-01-17 13:26:17 +01:00
Koen d64cd98b43 Removed most announcements. 2024-01-17 13:08:25 +01:00
Koen 0081ff1483 Removed playstore pre-installed PeerTube. 2024-01-17 12:54:45 +01:00
Kelvin f78ca6c7ed Toggle to disable update check for individual sources 2024-01-17 12:34:58 +01:00
Kelvin cfc7cbcaa4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 12:15:26 +01:00
Kelvin e533eb7778 Video zoom increase tolerances 2024-01-17 12:15:17 +01:00
Koen 7c1d0a7f88 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 11:02:13 +01:00
Koen 01ef471708 Better handling of null or empty id/url for Polycentric comments/likes. 2024-01-17 11:02:03 +01:00
Kelvin 2fd0a9a41d Fix scroll downloaded playlists 2024-01-16 22:31:45 +01:00
Kelvin 635749dfe4 Open channel/playlist urls through search 2024-01-16 21:42:43 +01:00
Kelvin c4bd5626f3 Fix usable motionlayout on portrait fullscreen 2024-01-16 21:21:26 +01:00
Kelvin 568a0f6329 Catch any failed auth/captcha decodes 2024-01-16 21:11:11 +01:00
Kelvin 7ee67b5cd0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 20:11:27 +01:00
Kelvin fc94c6903c Additional warnings for reinstall, disable uninstall if doesn't make sense, reduce maxheight to 40% of videoview 2024-01-16 20:11:23 +01:00
Koen a0af8805e7 Removed accidentally cmomitted code. 2024-01-16 20:09:02 +01:00
Koen 9b64cde17d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 20:08:15 +01:00
Koen f6931bcf8c Added isUserGesturing boolean to gesture control view. 2024-01-16 20:07:26 +01:00
Kelvin a4ff47d863 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 16:46:40 +01:00
Kelvin 982d251126 Prevent video reload if same video, refs 2024-01-16 16:46:30 +01:00
Koen 8820a0ecc0 Fixed slide subscription overlay not closing on back gesture in video detail view. Fixed bottom margin for minimized view progress bar. 2024-01-16 13:25:12 +01:00
Koen b99a713ffc Added 'Add to new playlist' to add button when watching video. 2024-01-16 13:04:44 +01:00
Koen dfc8c4b740 Added snapping when only panning. 2024-01-16 12:24:07 +01:00
Koen c3df9e5259 Changed tolerances on zooming/panning snapping. 2024-01-16 12:20:04 +01:00
Koen b9c7e0a8ca Made snapping only work when panned close to center. 2024-01-16 12:05:04 +01:00
Koen 2c7f02a24d Added zoom pan snapping. 2024-01-16 11:01:00 +01:00
Koen 5cc8488d94 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-15 18:07:08 +01:00
Koen 6f7304f59c Added ability to click on a comment to view where the comment was placed and the ability to navigate upwards in the replies overlay by clicking the parent comment. 2024-01-15 18:06:57 +01:00
Kelvin ea4fea4401 Deduplication priorities fixed, playpause button change and wakelock on interruption fixed 2024-01-13 00:51:00 +01:00
Kelvin 9b48664de4 Resolving wakelock on play interuptions 2024-01-12 23:22:49 +01:00
Kelvin 8964dc68f0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-12 21:34:49 +01:00
Kelvin 4711b8055b Empty home and install plugin flows. BrowserFragment optional url handling 2024-01-12 21:34:39 +01:00
Koen 84e3373fa7 Added settings to enable/disable zoom/pan gestures. 2024-01-11 15:48:58 +01:00
Koen fdd7e32dd8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-11 12:59:18 +01:00
Koen e57119ebbd Added setting for restore system brightness and finished zoom pan controls. 2024-01-11 12:59:10 +01:00
Kelvin ed29dd8365 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-10 17:53:52 +01:00
Kelvin 196cacb452 Open playlist urls support, indexOutOfBound fix for queue when deleting items 2024-01-10 17:53:45 +01:00
Koen c025913fc8 Fixed tutorial video aspect ratio. 2024-01-10 13:13:25 +01:00
Koen 48b2c68e72 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-10 13:07:33 +01:00
Koen 689766a6ac Added monetization tutorial and added tutorial descriptions. 2024-01-10 13:07:22 +01:00
Kelvin 9306024d17 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-09 22:25:49 +01:00
Kelvin 195163840b DOMParser toNodeTree, Better subscription errors, Watch later ordering 2024-01-09 22:25:41 +01:00
Koen 788c54bf8f Fixed durations for castview. 2024-01-09 13:50:51 +01:00
Koen 031aabd523 Proper implementation of editor action. 2024-01-09 13:37:56 +01:00
Koen 85db4cc4e6 Theoretical fix for double controls. 2024-01-08 14:19:21 +01:00
Koen 745aad385b Gesture controls can individually be enabled/disabled and can be toggled to use system or non-system values. 2024-01-08 13:56:11 +01:00
Koen ba87261f9f Added settings to enable/disable gestures. 2024-01-08 12:30:04 +01:00
Koen 7d091382c0 Do not restart threads when started is false. 2024-01-07 21:56:56 +01:00
Koen 781d0797e7 More casting fixes. 2024-01-07 18:18:15 +01:00
Koen ec12a06b88 Reverted disconnect based on pong timer. 2024-01-07 17:05:35 +01:00
Koen bf3e8867c3 Synchronized writes. 2024-01-07 14:19:03 +01:00
Koen 176814a715 Crashfix on unreliable casting connection. Made casting more robust with intermittent TCP connections. 2024-01-07 13:41:43 +01:00
Koen 898637a616 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-06 23:07:50 +01:00
Koen f1860126a7 - Changed casting connection timeout to 2 seconds.
- Added ping pong to FCast.
- Removal of DataInputStream and just using raw InputStream.
- Fix slider position crash.
2024-01-06 23:07:34 +01:00
Kelvin f8402676d7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-06 22:19:38 +01:00
Kelvin cf86ce1ab3 Minor leak fix, login warning support, refs 2024-01-06 22:19:29 +01:00
Koen f4cb1719e0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-04 13:38:37 +01:00
Koen 4898cb53ae Fixed tint color. 2024-01-04 13:38:30 +01:00
Kelvin 0f60d4737e Plugin update appToast, Refs 2024-01-03 19:47:38 +01:00
Kelvin 0dc33e1f2b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-03 17:59:11 +01:00
Kelvin d86a997a88 appToast system, VideoToOpen changed 2024-01-03 17:59:01 +01:00
Koen 34d4d92289 Casting stability fixes to ChromeCast connection thread. 2024-01-03 11:24:55 +01:00
Koen 4cb1bf268f Updated YouTube submodule. 2024-01-03 11:09:57 +01:00
Kelvin 8488706ff9 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 20:14:36 +01:00
Kelvin a348bb2662 Fix crash download livestream, refs 2024-01-02 20:14:28 +01:00
Koen 60a17b3c67 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 19:48:51 +01:00
Koen 386c58d4ad Added timeouts to casting flow. 2024-01-02 19:48:43 +01:00
Kelvin 356ba01dc1 Fix default comment section 2024-01-02 16:28:08 +01:00
Kelvin ed2aa848da Fix clear playback notification, Update V8 Library, Close modified requests after usage 2024-01-02 15:55:29 +01:00
Kelvin c5dd90048f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 14:36:46 +01:00
Kelvin ab04f334dc Download cleanup after cancel/failure and on startup, auto-select subtitles if downloaded, refs 2024-01-02 14:36:41 +01:00
Koen 0d44f8a416 Fixed issue where some videos would not play in Odysee. 2024-01-02 13:36:43 +01:00
Koen d01a1545e2 Allow large heap to fix Rumble failing to load large videos. 2024-01-02 13:35:48 +01:00
Kelvin e599729ba1 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-27 15:21:52 +01:00
Kelvin 3ac043740e Fix RequestModifier not being applied, and add default option to add pre-existing headers 2023-12-27 15:21:43 +01:00
Koen 89603d0ff3 changed typeface when update is available. 2023-12-27 14:31:26 +01:00
Koen 05b6cd7c97 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-27 11:11:36 +01:00
Koen ea5aad0631 Implemented check for updates. 2023-12-27 11:11:29 +01:00
Kelvin 96e034b9bf Fix plugin.d.ts and update 2023-12-22 19:29:38 +01:00
Kelvin 6141c36855 Refs 2023-12-21 20:04:53 +01:00
Kelvin 4084ab3ed0 refs 2023-12-21 20:01:51 +01:00
Kelvin 34e733823a Refs 2023-12-21 19:27:13 +01:00
Kelvin f1d01642cd Refs, ui fixes 2023-12-21 19:24:30 +01:00
Kelvin d5551d7118 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 17:24:18 +01:00
Kelvin d079a1e8e4 Add missing channel name setter 2023-12-21 17:24:11 +01:00
Koen c06c00ee9b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 17:19:17 +01:00
Koen 1d8eababc2 Minor casting dialog fixes. 2023-12-21 17:19:05 +01:00
Kelvin 75cf1ffbdd Offline playback fix with remote sources 2023-12-21 17:13:43 +01:00
Kelvin 5499706a9b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 16:14:01 +01:00
Kelvin ba57e32920 Minor queue styling, refs 2023-12-21 16:13:53 +01:00
Koen df96c5b51c Fixed live video previews in preview layout. Fixed time bar spacing at bottom. 2023-12-21 16:07:03 +01:00
Koen 75f81d20db Fixed buttons in subscription groups and made select button click only work when there are things selected. 2023-12-21 13:10:38 +01:00
Koen 3fc92e4065 Fixed rounding of subscription groups. 2023-12-21 12:56:35 +01:00
Koen 8ffd5f411f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 11:21:34 +01:00
Koen 918161a299 Fixed margins for subscription groups and menu bar icons now show filled variant if existing. 2023-12-21 11:20:53 +01:00
Kelvin 9f50f72eaa Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 11:12:45 +01:00
Kelvin 2f66f124aa Toast on cancel creation, default subgroup creator image, fix subgroup cache load 2023-12-21 11:12:34 +01:00
Koen 9a11717cf4 Fixed Rumble channel contents having empty author. Changed TutorialVideo so author is not clickable. 2023-12-21 10:20:52 +01:00
Kelvin 0d80424799 Optimized selection creator overlay, global subscription progress listener 2023-12-20 21:54:39 +01:00
Kelvin ed9a65b2f0 Filter cache results by enabled clients 2023-12-20 18:35:13 +01:00
Kelvin 8a53297be2 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 18:08:52 +01:00
Kelvin 20862a27c8 Search creator overlay, Fix crash Add topbar 2023-12-20 18:08:40 +01:00
Koen 95785e6c78 Added something more similar to Jdenticons. 2023-12-20 17:35:57 +01:00
Koen e88c649578 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 17:09:19 +01:00
Koen 09f91e64fb Fixed Kick subscription imports. 2023-12-20 17:04:10 +01:00
Koen b8923e59a1 Fixed bottom bar new tabs not showing up for people who changed tabs. 2023-12-20 17:03:48 +01:00
Kelvin e722c0ce9a Explore subscription groups button 2023-12-20 17:02:40 +01:00
Kelvin 56248bf4b0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 12:36:12 +01:00
Kelvin 5af4787c45 Fix nested overlays in more buttons 2023-12-20 12:35:40 +01:00
Koen 0990247322 Fixed Rumble channel content fetching, channel image and show comments when sign in is not required. 2023-12-20 12:06:21 +01:00
Koen 0154525578 Revert "Fixed download button overlay not working in more button."
This reverts commit 1dc6eee242.
2023-12-20 12:02:35 +01:00
Koen 1dc6eee242 Fixed download button overlay not working in more button. 2023-12-20 11:51:28 +01:00
Koen c63a63cb33 Fixed Gesture control distances for portrait full screen. 2023-12-20 11:06:18 +01:00
Koen c1967556ac Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 10:56:28 +01:00
Koen 309a57f5a1 Added icon based on the system key when a polycentric user has not set a profile picture. Made managing Polycentric profile most robust. Fixed an issue where latest events were not always being shown in relation to Polycentric profiles. Added copyable (on long press) system key in Polycentric profile activity. Added tutorial videos tab and flow. 2023-12-20 10:56:16 +01:00
Kelvin ee0bc96e53 Support for initial skip chapters 2023-12-20 00:28:21 +01:00
Kelvin a4422fdd56 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-19 23:08:48 +01:00
Kelvin b7c4047f1d Progress bars subscription groups, Various notification permission requests, improved notification experience, Settings activity open to specific settings, refs 2023-12-19 23:08:38 +01:00
Koen 65174ffc97 Show connected controls as disabled when still connecting. 2023-12-19 11:46:40 +01:00
Koen eac3e37af5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-19 11:38:32 +01:00
Koen 0d5ad90ff9 Fixed duration format. Added tutorial fragment. Added dialog that asks you if you want to see the tutorials on the first app boot. Made casting when clicking start now open connected dialog. Fixed crashes related to sliders in casting connected control. Fixed history tab title. Removed old FCast encryption. 2023-12-19 11:38:22 +01:00
Kelvin f42b14e95a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-18 23:37:27 +01:00
Kelvin b8acd0b5b2 Subscriptions fetching support for subgroups 2023-12-18 23:37:10 +01:00
Koen ef72561768 Added support for chapter skip in casting. 2023-12-18 16:28:35 +01:00
Koen d63627bd61 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-18 15:48:17 +01:00
Koen 422cceb225 Added support for FCast encrypted connection upgrade and added support for ensuring threads are restarted onResume for FCast and ChromeCAst. 2023-12-18 15:48:05 +01:00
Kelvin 76f5962232 Temp remove niconico 2023-12-15 22:47:04 +01:00
Kelvin 30df22d225 Fixing after merge 2023-12-15 22:44:24 +01:00
Kelvin cd4295be59 Merge branch 'video-cookie-support' into 'master'
Video Cookie Support & Static Request Modifiers

See merge request videostreaming/grayjay!11
2023-12-15 21:33:06 +00:00
Kelvin 7d366110b1 Merge branch 'wip-subgroups' into 'master'
Subscription Groups

See merge request videostreaming/grayjay!12
2023-12-15 21:28:03 +00:00
Kelvin 35c5045b3f Named presets 2023-12-15 20:10:28 +01:00
Kelvin 4930ea8183 Working subscription groups and image pickers 2023-12-15 19:38:33 +01:00
Kelvin 02292fed04 Progress 2023-12-13 23:28:34 +01:00
Koen bf6e61ed90 Fixed playback controls. 2023-12-13 17:14:40 +01:00
Koen 2ac8e0e621 Added casting controls to connected dialog. 2023-12-13 15:02:49 +01:00
Koen 0432f06eb3 Merge branch 'media3-migration' into 'master'
Media3 migration.

See merge request videostreaming/grayjay!10
2023-12-13 12:57:43 +00:00
Koen 7bfab8409f Made FCastCastingDevice more able to reconnect. 2023-12-13 13:47:38 +01:00
Koen 52d833d726 Added next and previous buttons to cast controls. 2023-12-13 13:40:11 +01:00
Koen 14d579eb1b Added support for playback rate changing when casting. 2023-12-13 13:28:51 +01:00
Koen d3ab8ecf3a Added support for automatically resuming casting device if stopped. 2023-12-13 12:48:39 +01:00
Koen 627b8c2b5d Implemented support for automatically connecting using QR code. 2023-12-13 12:36:16 +01:00
Koen 7f1cb22c12 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into media3-migration 2023-12-13 12:22:45 +01:00
Koen 5551bd31fe Added setting to allow full-screen portrait. 2023-12-13 12:18:54 +01:00
Koen 189d855c3f Automatic playback continuation should only happen when it was not paused or stopped. 2023-12-13 11:27:21 +01:00
Koen 0ab52e8f4d Fixed notification implementation. 2023-12-13 10:34:14 +01:00
Kelvin 27eb5aa6e1 WIP views 2023-12-12 22:20:38 +01:00
Kelvin 49b5b16641 Abstract RequestModifier, AdHocRequestModifier, RequestModifierOptions 2023-12-12 14:44:07 +01:00
Kelvin 73dd52af28 Working video cookies 2023-12-11 23:35:59 +01:00
Koen 3b8d256bad Media3 migration. 2023-12-11 17:54:18 +01:00
Kelvin 5d7dc1fdcb Language fixes, now uses standard language tags 2023-12-11 16:52:31 +01:00
Kelvin f31b6c50e9 Solving some warning 2023-12-11 13:26:13 +01:00
Koen fa12f8277c Reduced amount of warnings. 2023-12-11 11:51:43 +01:00
Koen 150a7d5006 Added proper permissions for download and export service (A14). 2023-12-10 09:29:33 +01:00
Koen a0a73a8e5c Fixes Android SDK. 2023-12-10 00:34:34 +01:00
Kelvin 4723a0b29a Fix up next view 2023-12-09 19:03:47 +01:00
Kelvin adbe0357ba Refs 2023-12-09 18:12:36 +01:00
Kelvin b0a35bcf3f No notification on known item, Polycentric logging, refs 2023-12-09 18:11:45 +01:00
Kelvin 0e7482321c Import platform redirect and disabled buttons, minor ui fixes 2023-12-09 16:31:34 +01:00
Koen e50d195b85 Fixed artwork not displaying properly. Loop button now hidden if you have a queue. Videos on queue editor now properly updates the amount of videos when a video is deleted. 2023-12-08 21:42:27 +01:00
Koen 33780f1046 Cleanup. 2023-12-08 14:53:57 +01:00
409 changed files with 11883 additions and 3999 deletions
+6 -3
View File
@@ -1,9 +1,6 @@
[submodule "dep/polycentricandroid"] [submodule "dep/polycentricandroid"]
path = dep/polycentricandroid path = dep/polycentricandroid
url = ../polycentricandroid.git url = ../polycentricandroid.git
[submodule "app/src/playstore/assets/sources/peertube"]
path = app/src/playstore/assets/sources/peertube
url = ../plugins/peertube.git
[submodule "app/src/stable/assets/sources/kick"] [submodule "app/src/stable/assets/sources/kick"]
path = app/src/stable/assets/sources/kick path = app/src/stable/assets/sources/kick
url = ../plugins/kick.git url = ../plugins/kick.git
@@ -61,3 +58,9 @@
[submodule "dep/futopay"] [submodule "dep/futopay"]
path = dep/futopay path = dep/futopay
url = ../futopayclientlibraries.git url = ../futopayclientlibraries.git
[submodule "app/src/unstable/assets/sources/bilibili"]
path = app/src/unstable/assets/sources/bilibili
url = ../plugins/bilibili.git
[submodule "app/src/stable/assets/sources/bilibili"]
path = app/src/stable/assets/sources/bilibili
url = ../plugins/bilibili.git
+12 -11
View File
@@ -151,7 +151,7 @@ dependencies {
//Core //Core
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.10.0' implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
//Images //Images
@@ -169,18 +169,19 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject) implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
//JS //JS
implementation("com.caoccao.javet:javet-android:2.2.1") implementation("com.caoccao.javet:javet-android:3.0.2")
//Exoplayer //Exoplayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' implementation 'androidx.media3:media3-exoplayer:1.2.1'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1' implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1' implementation 'androidx.media3:media3-ui:1.2.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1' implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.19.1' implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1' implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.19.1' implementation 'androidx.media3:media3-transformer:1.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5' implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.media:media:1.7.0'
//Other //Other
implementation 'org.jmdns:jmdns:3.5.1' implementation 'org.jmdns:jmdns:3.5.1'
@@ -0,0 +1,94 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "ffba56c2f572c25080ce8596e8bb8945",
"entities": [
{
"tableName": "history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT NOT NULL, `position` INTEGER NOT NULL, `datetime` INTEGER NOT NULL, `name` TEXT NOT NULL, `serialized` BLOB)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_history_url",
"unique": false,
"columnNames": [
"url"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_url` ON `${TABLE_NAME}` (`url`)"
},
{
"name": "index_history_name",
"unique": false,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_history_datetime",
"unique": false,
"columnNames": [
"datetime"
],
"orders": [
"DESC"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ffba56c2f572c25080ce8596e8bb8945')"
]
}
}
@@ -0,0 +1,88 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "eb813d54b9c44d29f1d7bb198a16d4d1",
"entities": [
{
"tableName": "subscription_cache",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT, `channelUrl` TEXT, `datetime` INTEGER, `serialized` BLOB)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "channelUrl",
"columnName": "channelUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_subscription_cache_url",
"unique": false,
"columnNames": [
"url"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_url` ON `${TABLE_NAME}` (`url`)"
},
{
"name": "index_subscription_cache_channelUrl",
"unique": false,
"columnNames": [
"channelUrl"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_channelUrl` ON `${TABLE_NAME}` (`channelUrl`)"
},
{
"name": "index_subscription_cache_datetime",
"unique": false,
"columnNames": [
"datetime"
],
"orders": [
"DESC"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb813d54b9c44d29f1d7bb198a16d4d1')"
]
}
}
@@ -0,0 +1,52 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "6e3b2d286325c4ea8a7a4c94c290daec",
"entities": [
{
"tableName": "testing",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`someString` TEXT NOT NULL, `someNum` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `serialized` BLOB)",
"fields": [
{
"fieldPath": "someString",
"columnName": "someString",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "someNum",
"columnName": "someNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e3b2d286325c4ea8a7a4c94c290daec')"
]
}
}
@@ -0,0 +1,111 @@
package com.futo.platformplayer
import android.util.Base64
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.futo.platformplayer.casting.FCastCastingDevice
import com.futo.platformplayer.casting.Opcode
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
import com.futo.platformplayer.casting.models.FCastKeyExchangeMessage
import com.futo.platformplayer.casting.models.FCastPlayMessage
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import javax.crypto.spec.SecretKeySpec
@RunWith(AndroidJUnit4::class)
class FCastEncryptionTests {
@Test
fun testDHEncryptionSelf() {
val keyPair1 = FCastCastingDevice.generateKeyPair()
val keyPair2 = FCastCastingDevice.generateKeyPair()
Log.i("testDHEncryptionSelf", "privates (1: ${Base64.encodeToString(keyPair1.private.encoded, Base64.NO_WRAP)}, 2: ${Base64.encodeToString(keyPair2.private.encoded, Base64.NO_WRAP)})")
val keyExchangeMessage1 = FCastCastingDevice.getKeyExchangeMessage(keyPair1)
val keyExchangeMessage2 = FCastCastingDevice.getKeyExchangeMessage(keyPair2)
Log.i("testDHEncryptionSelf", "publics (1: ${keyExchangeMessage1.publicKey}, 2: ${keyExchangeMessage2.publicKey})")
val aesKey1 = FCastCastingDevice.computeSharedSecret(keyPair1.private, keyExchangeMessage2)
val aesKey2 = FCastCastingDevice.computeSharedSecret(keyPair2.private, keyExchangeMessage1)
assertEquals(Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP), Base64.encodeToString(aesKey2.encoded, Base64.NO_WRAP))
Log.i("testDHEncryptionSelf", "aesKey ${Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)}")
val message = FCastPlayMessage("text/html")
val serializedBody = Json.encodeToString(message)
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
Log.i("testDHEncryptionSelf", Json.encodeToString(encryptedMessage))
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals(serializedBody, decryptedMessage.message)
}
@Test
fun testAESKeyGeneration() {
val cases = listOf(
listOf(
//Public other
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF",
//Private self
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=",
//AES
"7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk="
),
listOf(
//Public other
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK",
//Private self
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=",
//AES
"a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q="
)
)
for (case in cases) {
val decodedPrivateKey1 = Base64.decode(case[1], Base64.NO_WRAP)
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, case[0])
val keyFactory = KeyFactory.getInstance("DH")
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
val privateKey = keyFactory.generatePrivate(privateKeySpec)
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
assertEquals(case[2], Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
}
}
@Test
fun testDHEncryptionKnown() {
val decodedPrivateKey1 = Base64.decode("MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA==", Base64.NO_WRAP)
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=")
val keyFactory = KeyFactory.getInstance("DH")
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
val privateKey = keyFactory.generatePrivate(privateKeySpec)
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
assertEquals("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=", Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
val message = FCastPlayMessage("text/html")
val serializedBody = Json.encodeToString(message)
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals(serializedBody, decryptedMessage.message)
}
@Test
fun testDecryptMessageKnown() {
val encryptedMessage = Json.decodeFromString<FCastEncryptedMessage>("{\"version\":1,\"iv\":\"C4H70VC5FWrNtkty9/cLIA==\",\"blob\":\"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==\"}")
val aesKey = SecretKeySpec(Base64.decode("+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=", Base64.NO_WRAP), "AES")
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals("{\"container\":\"text/html\"}", decryptedMessage.message)
}
}
+11 -5
View File
@@ -7,10 +7,13 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/> <uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -22,7 +25,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.FutoVideo" android:theme="@style/Theme.FutoVideo"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31"
android:largeHeap="true">
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="@string/authority" android:authorities="@string/authority"
@@ -35,9 +39,11 @@
android:enabled="true" android:enabled="true"
android:foregroundServiceType="mediaPlayback" /> android:foregroundServiceType="mediaPlayback" />
<service android:name=".services.DownloadService" <service android:name=".services.DownloadService"
android:enabled="true" /> android:enabled="true"
android:foregroundServiceType="dataSync" />
<service android:name=".services.ExportingService" <service android:name=".services.ExportingService"
android:enabled="true" /> android:enabled="true"
android:foregroundServiceType="dataSync" />
<receiver android:name=".receivers.MediaControlReceiver" /> <receiver android:name=".receivers.MediaControlReceiver" />
<receiver android:name=".receivers.AudioNoisyReceiver" /> <receiver android:name=".receivers.AudioNoisyReceiver" />
@@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
function pluginRemoteCall(objID, methodName, args) { function pluginRemoteCall(objID, methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args))); return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
} }
function pluginRemoteTest(methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
}
function pluginIsLoggedIn(cb, err) { function pluginIsLoggedIn(cb, err) {
fetch("/plugin/isLoggedIn", { fetch("/plugin/isLoggedIn", {
+62 -2
View File
@@ -385,8 +385,8 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
<div style="width: 50%" v-if="Plugin.currentPlugin"> <div style="width: 50%" v-if="Plugin.currentPlugin">
<!--Get Home--> <v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
<v-card class="requestCard" v-for="req in Testing.requests"> <v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
<v-card-text> <v-card-text>
<div class="title"> <div class="title">
<span v-if="req.isOptional">(Optional)</span> <span v-if="req.isOptional">(Optional)</span>
@@ -402,6 +402,11 @@
<div class="code"> <div class="code">
{{req.code}} {{req.code}}
</div> </div>
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
<a :href="req.docUrl" target="_blank">
Documentation
</a>
</div>
<div> <div>
<div class="parameter" v-for="parameter in req.parameters"> <div class="parameter" v-for="parameter in req.parameters">
<div class="name"> <div class="name">
@@ -416,6 +421,9 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn @click="testSourceRemotely(req)">
Test Android
</v-btn>
<v-btn @click="testSource(req)"> <v-btn @click="testSource(req)">
Test Test
</v-btn> </v-btn>
@@ -535,6 +543,7 @@
<!--<script src="./dependencies/vue.js"></script>--> <!--<script src="./dependencies/vue.js"></script>-->
<!--<script src="./dependencies/vuetify.js"></script>--> <!--<script src="./dependencies/vuetify.js"></script>-->
<script src="./source_docs.js"></script> <script src="./source_docs.js"></script>
<script src="./source_doc_urls.js"></script>
<script src="./source.js"></script> <script src="./source.js"></script>
<script src="./dev_bridge.js"></script> <script src="./dev_bridge.js"></script>
<script> <script>
@@ -545,6 +554,7 @@
new Vue({ new Vue({
el: '#app', el: '#app',
data: { data: {
searchTestMethods: "",
page: "Plugin", page: "Plugin",
pastPluginUrls: [], pastPluginUrls: [],
settings: {}, settings: {},
@@ -570,6 +580,9 @@
Testing: { Testing: {
requests: sourceDocs.map(x=>{ requests: sourceDocs.map(x=>{
x.parameters.forEach(y=>y.value = null); x.parameters.forEach(y=>y.value = null);
if(sourceDocUrls[x.title])
x.docUrl = sourceDocUrls[x.title];
return x; return x;
}), }),
lastResult: "", lastResult: "",
@@ -860,6 +873,53 @@
"Error: " + ex; "Error: " + ex;
} }
}, },
testSourceRemotely(req) {
const name = req.title;
const parameterVals = req.parameters.map(x=>{
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
return JSON.parse(x.value.substring(5));
return x.value
});
if(name == "enable") {
if(parameterVals.length > 0)
parameterVals[0] = this.Plugin.currentPlugin;
else
parameterVals.push(this.Plugin.currentPlugin);
if(parameterVals.length > 1)
parameterVals[1] = __DEV_SETTINGS;
else
parameterVals.push(__DEV_SETTINGS);
}
const func = source[name];
if(!func)
alert("Test func not found");
try {
const remoteResult = pluginRemoteTest(name, parameterVals);
console.log("Result for " + req.title, remoteResult);
this.Testing.lastResult = "//Results [" + name + "]\n" +
JSON.stringify(remoteResult, null, 3);
this.Testing.lastResultError = "";
}
catch(ex) {
if(ex.plugin_type == "CaptchaRequiredException") {
let shouldCaptcha = confirm("Do you want to request captcha?");
if(shouldCaptcha) {
pluginCaptchaTestPlugin(ex.url, ex.body);
}
}
console.error("Failed to run test for " + req.title, ex);
this.Testing.lastResult = ""
if(ex.message)
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex.message + "\n\n" + ex.stack;
else
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex;
}
},
showTestResults(results) { showTestResults(results) {
}, },
+120 -46
View File
@@ -1,13 +1,37 @@
declare class ScriptException extends Error { declare class ScriptException extends Error {
//If only one parameter is provided, acts as msg
constructor(type: string, msg: string); constructor(type: string, msg: string);
} }
declare class TimeoutException extends ScriptException {
declare class LoginRequiredException extends ScriptException {
constructor(msg: string); constructor(msg: string);
} }
//Alias
declare class ScriptLoginRequiredException extends ScriptException {
constructor(msg: string);
}
declare class CaptchaRequiredException extends ScriptException {
constructor(url: string, body: string);
}
declare class CriticalException extends ScriptException {
constructor(msg: string);
}
declare class UnavailableException extends ScriptException { declare class UnavailableException extends ScriptException {
constructor(msg: string); constructor(msg: string);
} }
declare class AgeException extends ScriptException {
constructor(msg: string);
}
declare class TimeoutException extends ScriptException {
constructor(msg: string);
}
declare class ScriptImplementationException extends ScriptException { declare class ScriptImplementationException extends ScriptException {
constructor(msg: string); constructor(msg: string);
} }
@@ -38,16 +62,23 @@ declare class FilterCapability {
declare class PlatformAuthorLink { declare class PlatformAuthorLink {
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?); constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
}
declare class PlatformAuthorMembershipLink {
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
} }
declare interface PlatformContentDef { declare interface PlatformContentDef {
id: PlatformID, id: PlatformID,
name: string, name: string,
thumbnails: Thumbnails,
author: PlatformAuthorLink, author: PlatformAuthorLink,
datetime: integer, datetime: integer,
url: string url: string
} }
declare interface PlatformContent {}
declare interface PlatformNestedMediaContentDef extends PlatformContentDef { declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
contentUrl: string, contentUrl: string,
contentName: string?, contentName: string?,
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
constructor(obj: PlatformNestedMediaContentDef); constructor(obj: PlatformNestedMediaContentDef);
} }
declare interface PlatformLockedContentDef extends PlatformContentDef {
contentName: string?,
contentThumbnails: Thumbnails?,
unlockUrl: string,
lockDescription: string?,
}
declare class PlatformLockedContent {
constructor(obj: PlatformLockedContentDef);
}
declare interface PlatformVideoDef extends PlatformContentDef { declare interface PlatformVideoDef extends PlatformContentDef {
thumbnails: Thumbnails, thumbnails: Thumbnails,
author: PlatformAuthorLink, author: PlatformAuthorLink,
duration: int, duration: int,
viewCount: long, viewCount: long,
isLive: boolean isLive: boolean,
shareUrl: string?
} }
declare interface PlatformContent {}
declare class PlatformVideo implements PlatformContent { declare class PlatformVideo implements PlatformContent {
constructor(obj: PlatformVideoDef); constructor(obj: PlatformVideoDef);
} }
@@ -77,14 +118,15 @@ declare class PlatformVideo implements PlatformContent {
declare interface PlatformVideoDetailsDef extends PlatformVideoDef { declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
description: string, description: string,
video: VideoSourceDescriptor, video: VideoSourceDescriptor,
live: SubtitleSource[], live: IVideoSource,
rating: IRating rating: IRating,
subtitles: SubtitleSource[]
} }
declare class PlatformVideoDetails extends PlatformVideo { declare class PlatformVideoDetails extends PlatformVideo {
constructor(obj: PlatformVideoDetailsDef); constructor(obj: PlatformVideoDetailsDef);
} }
declare class PlatformPostDef extends PlatformContentDef { declare interface PlatformPostDef extends PlatformContentDef {
thumbnails: string[], thumbnails: string[],
images: string[], images: string[],
description: string description: string
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
constructor(obj: PlatformPostDef) constructor(obj: PlatformPostDef)
} }
declare class PlatformPostDetailsDef extends PlatformPostDef { declare interface PlatformPostDetailsDef extends PlatformPostDef {
rating: IRating, rating: IRating,
textType: int, textType: int,
content: String content: String
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
isUnMuxed: boolean, isUnMuxed: boolean,
videoSources: VideoSource[] videoSources: VideoSource[]
} }
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor { declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
constructor(obj: VideoSourceDescriptorDef); constructor(videoSourcesOrObj: VideoSource[]);
} }
declare interface UnMuxVideoSourceDescriptorDef { declare interface UnMuxVideoSourceDescriptorDef {
@@ -129,7 +171,7 @@ declare interface IVideoSource {
declare interface IAudioSource { declare interface IAudioSource {
} }
interface VideoUrlSourceDef implements IVideoSource { declare interface VideoUrlSourceDef implements IVideoSource {
width: integer, width: integer,
height: integer, height: integer,
container: string, container: string,
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
duration: integer, duration: integer,
url: string url: string
} }
class VideoUrlSource { declare class VideoUrlSource {
constructor(obj: VideoUrlSourceDef); constructor(obj: VideoUrlSourceDef);
getRequestModifier(): RequestModifier?; getRequestModifier(): RequestModifier?;
} }
interface VideoUrlRangeSourceDef extends VideoUrlSource { declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
itagId: integer, itagId: integer,
initStart: integer, initStart: integer,
initEnd: integer, initEnd: integer,
indexStart: integer, indexStart: integer,
indexEnd: integer, indexEnd: integer,
} }
class VideoUrlRangeSource extends VideoUrlSource { declare class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj: YTVideoSourceDef); constructor(obj: YTVideoSourceDef);
} }
interface AudioUrlSourceDef { declare interface AudioUrlSourceDef {
name: string, name: string,
bitrate: integer, bitrate: integer,
container: string, container: string,
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
url: string, url: string,
language: string language: string
} }
class AudioUrlSource implements IAudioSource { declare class AudioUrlSource implements IAudioSource {
constructor(obj: AudioUrlSourceDef); constructor(obj: AudioUrlSourceDef);
getRequestModifier(): RequestModifier?; getRequestModifier(): RequestModifier?;
} }
interface IRequest { declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
url: string,
headers: Map<string, string>
}
interface IRequestModifierDef {
allowByteSkip: boolean
}
class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
interface AudioUrlRangeSourceDef extends AudioUrlSource {
itagId: integer, itagId: integer,
initStart: integer, initStart: integer,
initEnd: integer, initEnd: integer,
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
indexEnd: integer, indexEnd: integer,
audioChannels: integer audioChannels: integer
} }
class AudioUrlRangeSource extends AudioUrlSource { declare class AudioUrlRangeSource extends AudioUrlSource {
constructor(obj: AudioUrlRangeSourceDef); constructor(obj: AudioUrlRangeSourceDef);
} }
interface HLSSourceDef { declare interface HLSSourceDef {
name: string, name: string,
duration: integer, duration: integer,
url: string url: string,
priority: boolean?,
language: string?
} }
class HLSSource implements IVideoSource { declare class HLSSource implements IVideoSource {
constructor(obj: HLSSourceDef); constructor(obj: HLSSourceDef);
} }
interface DashSourceDef { declare interface DashSourceDef {
name: string, name: string,
duration: integer, duration: integer,
url: string url: string,
language: string?
} }
class DashSource implements IVideoSource { declare class DashSource implements IVideoSource {
constructor(obj: DashSourceDef) constructor(obj: DashSourceDef)
} }
declare interface IRequest {
url: string,
headers: Map<string, string>
}
declare interface IRequestModifierDef {
allowByteSkip: boolean
}
declare class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
//Channel //Channel
interface PlatformChannelDef { declare interface PlatformChannelDef {
id: PlatformID, id: PlatformID,
name: string, name: string,
thumbnail: string, thumbnail: string,
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
subscribers: integer, subscribers: integer,
description: string, description: string,
url: string, url: string,
urlAlternatives: string[],
links: Map<string>? links: Map<string>?
} }
class PlatformChannel { declare class PlatformChannel {
constructor(obj: PlatformChannelDef); constructor(obj: PlatformChannelDef);
} }
//Playlist
declare interface PlatformPlaylistDef implements PlatformContent {
videoCount: integer,
thumbnail: string
}
declare class PlatformPlaylist extends PlatformContent {
constructor(obj: PlatformPlaylistDef);
}
declare interface PlatformPlaylistDetailsDef implements PlatformPlaylistDef {
contents: ContentPager
}
declare class PlatformPlaylistDetails extends PlatformContent {
constructor(obj: PlatformPlaylistDetailsDef);
}
//Ratings //Ratings
interface IRating { interface IRating {
type: integer type: integer
@@ -250,7 +313,11 @@ declare class PlatformComment {
constructor(obj: CommentDef); constructor(obj: CommentDef);
} }
declare class PlaybackTracker {
constructor(interval: integer);
setProgress(seconds: integer);
}
declare class LiveEventPager { declare class LiveEventPager {
nextRequest = 4000; nextRequest = 4000;
@@ -261,8 +328,8 @@ declare class LiveEventPager {
nextPage(): LiveEventPager; //Could be self nextPage(): LiveEventPager; //Could be self
} }
class LiveEvent { declare class LiveEvent {
type: String constructor(type: integer);
} }
declare class LiveEventComment extends LiveEvent { declare class LiveEventComment extends LiveEvent {
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]); constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
@@ -287,25 +354,31 @@ declare class ContentPager {
constructor(results: PlatformContent[], hasMore: boolean); constructor(results: PlatformContent[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): VideoPager; //Could be self nextPage(): ContentPager?; //Could be self
} }
declare class VideoPager { declare class VideoPager {
constructor(results: PlatformVideo[], hasMore: boolean); constructor(results: PlatformVideo[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): VideoPager; //Could be self nextPage(): VideoPager?; //Could be self
} }
declare class ChannelPager { declare class ChannelPager {
constructor(results: PlatformChannel[], hasMore: boolean); constructor(results: PlatformChannel[], hasMore: boolean);
hasMorePagers(): boolean; hasMorePagers(): boolean;
nextPage(): ChannelPager; //Could be self nextPage(): ChannelPager?; //Could be self
}
declare class PlaylistPager {
constructor(results: PlatformPlaylist[], hasMore: boolean);
hasMorePagers(): boolean;
nextPage(): PlaylistPager?;
} }
declare class CommentPager { declare class CommentPager {
constructor(results: PlatformComment[], hasMore: boolean); constructor(results: PlatformComment[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): CommentPager; //Could be self nextPage(): CommentPager?; //Could be self
} }
interface Map<T> { interface Map<T> {
@@ -341,8 +414,9 @@ interface Source {
getChannelCapabilities(): ResultCapabilities; getChannelCapabilities(): ResultCapabilities;
isContentDetailsUrl(url: string): boolean; isContentDetailsUrl(url: string): boolean;
getContentDetails(url: string): PlatformVideoDetails; getContentDetails(url: string): PlatformContentDetails;
//Optional
getLiveEvents(url: string): LiveEventPager; getLiveEvents(url: string): LiveEventPager;
//Optional //Optional
+31 -16
View File
@@ -37,24 +37,26 @@ let Type = {
NORMAL: 0, NORMAL: 0,
SKIPPABLE: 5, SKIPPABLE: 5,
SKIP: 6 SKIP: 6,
SKIPONCE: 7
} }
}; };
let Language = { let Language = {
UNKNOWN: "Unknown", UNKNOWN: "Unknown",
ARABIC: "Arabic", ARABIC: "ar",
SPANISH: "Spanish", SPANISH: "es",
FRENCH: "French", FRENCH: "fr",
HINDI: "Hindi", HINDI: "hi",
INDONESIAN: "Indonesian", INDONESIAN: "id",
KOREAN: "Korean", KOREAN: "ko",
PORTBRAZIL: "Portuguese Brazilian", PORTUGUESE: "pt",
RUSSIAN: "Russian", PORTBRAZIL: "pt",
THAI: "Thai", RUSSIAN: "ru",
TURKISH: "Turkish", THAI: "th",
VIETNAMESE: "Vietnamese", TURKISH: "tr",
ENGLISH: "English" VIETNAMESE: "vi",
ENGLISH: "en"
} }
class ScriptException extends Error { class ScriptException extends Error {
@@ -76,6 +78,11 @@ class ScriptLoginRequiredException extends ScriptException {
super("ScriptLoginRequiredException", msg); super("ScriptLoginRequiredException", msg);
} }
} }
class LoginRequiredException extends ScriptException {
constructor(msg) {
super("ScriptLoginRequiredException", msg);
}
}
class CaptchaRequiredException extends Error { class CaptchaRequiredException extends Error {
constructor(url, body) { constructor(url, body) {
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body })); super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
@@ -247,8 +254,8 @@ class PlatformVideoDetails extends PlatformVideo {
this.description = obj.description ?? "";//String this.description = obj.description ?? "";//String
this.video = obj.video ?? {}; //VideoSourceDescriptor this.video = obj.video ?? {}; //VideoSourceDescriptor
this.dash = obj.dash ?? null; //DashSource this.dash = obj.dash ?? null; //DashSource, deprecated
this.hls = obj.hls ?? null; //HLSSource this.hls = obj.hls ?? null; //HLSSource, deprecated
this.live = obj.live ?? null; //VideoSource this.live = obj.live ?? null; //VideoSource
this.rating = obj.rating ?? null; //IRating this.rating = obj.rating ?? null; //IRating
@@ -319,6 +326,8 @@ class VideoUrlSource {
this.bitrate = obj.bitrate ?? 0; this.bitrate = obj.bitrate ?? 0;
this.duration = obj.duration ?? 0; this.duration = obj.duration ?? 0;
this.url = obj.url; this.url = obj.url;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class VideoUrlRangeSource extends VideoUrlSource { class VideoUrlRangeSource extends VideoUrlSource {
@@ -344,6 +353,8 @@ class AudioUrlSource {
this.duration = obj.duration ?? 0; this.duration = obj.duration ?? 0;
this.url = obj.url; this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN; this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class AudioUrlRangeSource extends AudioUrlSource { class AudioUrlRangeSource extends AudioUrlSource {
@@ -369,6 +380,8 @@ class HLSSource {
this.priority = obj.priority ?? false; this.priority = obj.priority ?? false;
if(obj.language) if(obj.language)
this.language = obj.language; this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class DashSource { class DashSource {
@@ -380,13 +393,15 @@ class DashSource {
this.url = obj.url; this.url = obj.url;
if(obj.language) if(obj.language)
this.language = obj.language; this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class RequestModifier { class RequestModifier {
constructor(obj) { constructor(obj) {
obj = obj ?? {}; obj = obj ?? {};
this.allowByteSkip = obj.allowByteSkip; this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
} }
} }
@@ -1,15 +1,28 @@
package com.futo.platformplayer import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this); fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this); fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this); fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
@UnstableApi
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier();
return if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else {
DefaultHttpDataSource.Factory();
}
}
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any()); fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
@@ -13,7 +13,8 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.time.toDuration import kotlin.math.roundToInt
import kotlin.math.roundToLong
//Long //Long
@@ -120,7 +121,8 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now()); return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
} }
fun OffsetDateTime.getNowDiffYears(): Long { fun OffsetDateTime.getNowDiffYears(): Long {
return ChronoUnit.YEARS.between(this, OffsetDateTime.now()); val diff = ChronoUnit.MONTHS.between(this, OffsetDateTime.now()) / 12.0;
return diff.roundToLong();
} }
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long { fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
@@ -151,6 +153,7 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
if(value >= secondsInYear) { if(value >= secondsInYear) {
value = getNowDiffYears(); value = getNowDiffYears();
if(abs) value = abs(value); if(abs) value = abs(value);
value = Math.max(1, value);
unit = "year"; unit = "year";
} }
else if(value >= secondsInMonth) { else if(value >= secondsInMonth) {
@@ -228,6 +231,18 @@ fun String.fixHtmlWhitespace(): Spanned {
return Html.fromHtml(replace("\n", "<br />"), HtmlCompat.FROM_HTML_MODE_LEGACY); return Html.fromHtml(replace("\n", "<br />"), HtmlCompat.FROM_HTML_MODE_LEGACY);
} }
fun Long.formatDuration(): String {
val hours = this / 3600000
val minutes = (this % 3600000) / 60000
val seconds = (this % 60000) / 1000
return if (hours > 0) {
String.format("%02d:%02d:%02d", hours, minutes, seconds)
} else {
String.format("%02d:%02d", minutes, seconds)
}
}
fun String.fixHtmlLinks(): Spanned { fun String.fixHtmlLinks(): Spanned {
//TODO: Properly fix whitespace handling. //TODO: Properly fix whitespace handling.
val doc = Jsoup.parse(replace("\n", "<br />")); val doc = Jsoup.parse(replace("\n", "<br />"));
@@ -1,5 +1,6 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.util.Log
import com.google.common.base.CharMatcher import com.google.common.base.CharMatcher
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
@@ -9,7 +10,6 @@ import java.net.InetAddress
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.charset.Charset
private const val IPV4_PART_COUNT = 4; private const val IPV4_PART_COUNT = 4;
@@ -169,7 +169,7 @@ private fun parseHextet(ipString: String, start: Int, end: Int): Short {
var hextet = 0 var hextet = 0
for (i in start until end) { for (i in start until end) {
hextet = hextet shl 4 hextet = hextet shl 4
hextet = hextet or ipString[i].digitToIntOrNull(16)!! ?: -1 hextet = hextet or ipString[i].digitToIntOrNull(16)!!
} }
return hextet.toShort() return hextet.toShort()
} }
@@ -216,15 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
} }
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? { fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
val timeout = 2000
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
return null; return null;
} }
if (addresses.size == 1) { if (addresses.size == 1) {
val socket = Socket()
try { try {
return Socket(addresses[0], port); return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
} catch (e: Throwable) { } catch (e: Throwable) {
//Ignored. Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
socket.close()
} }
return null; return null;
@@ -249,7 +254,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
} }
} }
socket.connect(InetSocketAddress(address, port)); socket.connect(InetSocketAddress(address, port), timeout);
synchronized(syncObject) { synchronized(syncObject) {
if (connectedSocket == null) { if (connectedSocket == null) {
@@ -263,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {
//Ignore Log.i("getConnectedSocket", "Failed to connect to: $address", e)
} }
}; };
@@ -1,11 +1,13 @@
package com.futo.platformplayer package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.adapters.CommentViewHolder
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import userpackage.Protocol import userpackage.Protocol
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
@@ -47,6 +49,15 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
} }
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() { suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
removeServer(PolycentricCache.STAGING_SERVER)
}
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
removeServer(PolycentricCache.SERVER)
}
val exceptions = fullyBackfillServers() val exceptions = fullyBackfillServers()
for (pair in exceptions) { for (pair in exceptions) {
val server = pair.key val server = pair.key
@@ -27,7 +27,7 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
if(this !is T) if(this !is T)
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}"); throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
return this as T; return this;
} }
//Singles //Singles
@@ -0,0 +1,20 @@
package com.futo.platformplayer
class PresetImages {
companion object {
val images = mapOf<String, Int>(
Pair("xp_book", R.drawable.xp_book),
Pair("xp_forest", R.drawable.xp_forest),
Pair("xp_code", R.drawable.xp_code),
Pair("xp_controller", R.drawable.xp_controller),
Pair("xp_laptop", R.drawable.xp_laptop)
);
fun getPresetResIdByName(name: String): Int {
return images[name] ?: -1;
}
fun getPresetNameByResId(id: Int): String? {
return images.entries.find { it.value == id }?.key;
}
}
}
@@ -6,28 +6,41 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.* import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.states.* import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePayment
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.* import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -248,20 +261,23 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL; return FeedStyle.THUMBNAIL;
} }
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5) @FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 7) @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true; var fetchOnAppBoot: Boolean = true;
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 8) @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true; var fetchOnTabOpen: Boolean = true;
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 9) @FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10, "background_update")
@DropdownFieldOptionsId(R.array.background_interval) @DropdownFieldOptionsId(R.array.background_interval)
var subscriptionsBackgroundUpdateInterval: Int = 0; var subscriptionsBackgroundUpdateInterval: Int = 0;
@@ -277,7 +293,7 @@ class Settings : FragmentedStorageFileJson() {
}; };
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 10) @FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11)
@DropdownFieldOptionsId(R.array.thread_count) @DropdownFieldOptionsId(R.array.thread_count)
var subscriptionConcurrency: Int = 3; var subscriptionConcurrency: Int = 3;
@@ -285,17 +301,20 @@ class Settings : FragmentedStorageFileJson() {
return threadIndexToCount(subscriptionConcurrency); return threadIndexToCount(subscriptionConcurrency);
} }
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 11) @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false; var showWatchMetrics: Boolean = false;
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 12) @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true; var allowPlaytimeTracking: Boolean = true;
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 13) @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false; var alwaysReloadFromCache: Boolean = false;
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14) @FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false;
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
fun clearChannelCache() { fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing.."); UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear(); StateCache.instance.clear();
@@ -311,7 +330,28 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.audio_languages) @DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0; var primaryLanguage: Int = 0;
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.audio_languages)[primaryLanguage]; fun getPrimaryLanguage(context: Context): String? {
return when(primaryLanguage) {
0 -> "en";
1 -> "es";
2 -> "de";
3 -> "fr";
4 -> "ja";
5 -> "ko";
6 -> "th";
7 -> "vi";
8 -> "id";
9 -> "hi";
10 -> "ar";
11 -> "tu";
12 -> "ru";
13 -> "pt";
14 -> "zh";
else -> null
}
}
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1) @FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
@DropdownFieldOptionsId(R.array.playback_speeds) @DropdownFieldOptionsId(R.array.playback_speeds)
@@ -407,6 +447,9 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12) @FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss) @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterConnectivityLoss: Int = 1; var restartPlaybackAfterConnectivityLoss: Int = 1;
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false;
} }
@FormField(R.string.comments, "group", R.string.comments_description, 6) @FormField(R.string.comments, "group", R.string.comments_description, 6)
@@ -506,6 +549,8 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.log_levels) @DropdownFieldOptionsId(R.array.log_levels)
var logLevel: Int = 0; var logLevel: Int = 0;
fun isVerbose() = logLevel >= 4;
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1) @FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
fun submitLogs() { fun submitLogs() {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
@@ -555,7 +600,7 @@ class Settings : FragmentedStorageFileJson() {
val cookieManager: CookieManager = CookieManager.getInstance(); val cookieManager: CookieManager = CookieManager.getInstance();
cookieManager.removeAllCookies(null); cookieManager.removeAllCookies(null);
} }
@FormField(R.string.reinstall_embedded_plugins, FieldForm.BUTTON, R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1) /*@FormField(R.string.reinstall_embedded_plugins, FieldForm.BUTTON, R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1)
fun reinstallEmbedded() { fun reinstallEmbedded() {
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
try { try {
@@ -574,7 +619,7 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
} }
} }*/
} }
@@ -645,7 +690,9 @@ class Settings : FragmentedStorageFileJson() {
fun manualCheck() { fun manualCheck() {
if (!BuildConfig.IS_PLAYSTORE_BUILD) { if (!BuildConfig.IS_PLAYSTORE_BUILD) {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
StateUpdate.instance.checkForUpdates(it, true); StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(it, true)
}
} }
} else { } else {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
@@ -767,7 +814,36 @@ class Settings : FragmentedStorageFileJson() {
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
} }
@FormField(R.string.info, FieldForm.GROUP, -1, 19) @FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
var gestureControls = GestureControls();
@Serializable
class GestureControls {
@FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
var volumeSlider: Boolean = true;
@FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
var brightnessSlider: Boolean = true;
@FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
var toggleFullscreen: Boolean = true;
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
var useSystemBrightness: Boolean = false;
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
var useSystemVolume: Boolean = true;
@FormField(R.string.restore_system_brightness, FieldForm.TOGGLE, R.string.restore_system_brightness_descr, 6)
var restoreSystemBrightness: Boolean = true;
@FormField(R.string.zoom_option, FieldForm.TOGGLE, R.string.zoom_option_descr, 7)
var zoom: Boolean = true;
@FormField(R.string.pan_option, FieldForm.TOGGLE, R.string.pan_option_descr, 8)
var pan: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
var info = Info(); var info = Info();
@Serializable @Serializable
class Info { class Info {
@@ -2,15 +2,9 @@ package com.futo.platformplayer
import android.content.Context import android.content.Context
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope
import androidx.work.Constraints
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.futo.platformplayer.activities.DeveloperActivity import com.futo.platformplayer.activities.DeveloperActivity
@@ -36,7 +30,6 @@ import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.views.fields.ButtonField import com.futo.platformplayer.views.fields.ButtonField
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
@@ -44,11 +37,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.* import kotlinx.serialization.Contextual
import kotlinx.serialization.json.* import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.stream.IntStream.range import java.util.stream.IntStream.range
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@@ -109,14 +103,14 @@ class SettingsDev : FragmentedStorageFileJson() {
StateApp.instance.scope.launch(Dispatchers.IO) { StateApp.instance.scope.launch(Dispatchers.IO) {
try { try {
val subsCache = val subsCache =
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this)?.first; StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this).first;
var total = 0; var total = 0;
var page = 0; var page = 0;
var lastToast = System.currentTimeMillis(); var lastToast = System.currentTimeMillis();
while(subsCache!!.hasMorePages() && total < 5000) { while(subsCache.hasMorePages() && total < 5000) {
subsCache!!.nextPage(); subsCache.nextPage();
total += subsCache!!.getResults().size; total += subsCache.getResults().size;
page++; page++;
if(page % 10 == 0) if(page % 10 == 0)
@@ -174,9 +168,9 @@ class SettingsDev : FragmentedStorageFileJson() {
var total = 0; var total = 0;
var page = 0; var page = 0;
var lastToast = System.currentTimeMillis(); var lastToast = System.currentTimeMillis();
while(subsCache!!.hasMorePages() && total < 5000) { while(subsCache.hasMorePages() && total < 5000) {
subsCache!!.nextPage(); subsCache.nextPage();
total += subsCache!!.getResults().size; total += subsCache.getResults().size;
page++; page++;
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) { for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
@@ -375,9 +369,9 @@ class SettingsDev : FragmentedStorageFileJson() {
@FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2) @FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2)
fun testV8Home() { fun testV8Home() {
runTestPlugin(_currentPlugin) { runTestPlugin(_currentPlugin) {
var home: IPager<IPlatformContent>? = null; var home: IPager<IPlatformContent>?;
var resultPage1: String = ""; val resultPage1: String;
var resultPage2: String = ""; val resultPage2: String;
val page1Time = measureTimeMillis { val page1Time = measureTimeMillis {
home = it.getHome(); home = it.getHome();
val results = home!!.getResults(); val results = home!!.getResults();
@@ -11,17 +11,34 @@ import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.* import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.dialogs.* import com.futo.platformplayer.dialogs.AutoUpdateDialog
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
import com.futo.platformplayer.dialogs.AutomaticRestoreDialog
import com.futo.platformplayer.dialogs.CastingAddDialog
import com.futo.platformplayer.dialogs.CastingHelpDialog
import com.futo.platformplayer.dialogs.ChangelogDialog
import com.futo.platformplayer.dialogs.CommentDialog
import com.futo.platformplayer.dialogs.ConnectCastingDialog
import com.futo.platformplayer.dialogs.ConnectedCastingDialog
import com.futo.platformplayer.dialogs.ImportDialog
import com.futo.platformplayer.dialogs.ImportOptionsDialog
import com.futo.platformplayer.dialogs.MigrateDialog
import com.futo.platformplayer.dialogs.ProgressDialog
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -288,12 +305,16 @@ class UIDialogs {
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction) showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
} }
fun showUpdateAvailableDialog(context: Context, lastVersion: Int) { fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
val dialog = AutoUpdateDialog(context); val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
dialog.setMaxVersion(lastVersion); dialog.setMaxVersion(lastVersion);
if (hideExceptionButtons) {
dialog.hideExceptionButtons()
}
} }
fun showChangelogDialog(context: Context, lastVersion: Int) { fun showChangelogDialog(context: Context, lastVersion: Int) {
@@ -323,8 +344,8 @@ class UIDialogs {
} }
} }
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) { fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded); val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
@@ -341,11 +362,17 @@ class UIDialogs {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null) { if (d != null) {
val dialog = ConnectedCastingDialog(context); val dialog = ConnectedCastingDialog(context);
if (context is Activity) {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} else { } else {
val dialog = ConnectCastingDialog(context); val dialog = ConnectCastingDialog(context);
if (context is Activity) {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog); registerDialogOpened(dialog);
val c = context val c = context
if (c is Activity) { if (c is Activity) {
@@ -377,13 +404,28 @@ class UIDialogs {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try { try {
StateApp.withContext { StateApp.withContext {
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show(); toast(it, text, long);
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e); Logger.e(TAG, "Failed to show toast.", e);
} }
} }
} }
fun appToast(text: String, long: Boolean = false) {
appToast(ToastView.Toast(text, long))
}
fun appToastError(text: String, long: Boolean) {
StateApp.withContext {
appToast(ToastView.Toast(text, long, it.getColor(R.color.pastel_red)));
};
}
fun appToast(toast: ToastView.Toast) {
StateApp.withContext {
if(it is MainActivity) {
it.showAppToast(toast);
}
}
}
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) { fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
//TODO: Is not actually clickable... //TODO: Is not actually clickable...
@@ -1,10 +1,17 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.app.NotificationManager
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
@@ -17,31 +24,46 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.states.* import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.futo.platformplayer.views.pills.RoundButton import com.futo.platformplayer.views.pills.RoundButton
import com.futo.platformplayer.views.pills.RoundButtonGroup import com.futo.platformplayer.views.pills.RoundButtonGroup
import com.futo.platformplayer.views.overlays.slideup.*
import com.futo.platformplayer.views.video.FutoVideoPlayerBase import com.futo.platformplayer.views.video.FutoVideoPlayerBase
import isDownloadable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.IllegalStateException
class UISlideOverlays { class UISlideOverlays {
companion object { companion object {
private const val TAG = "UISlideOverlays"; private const val TAG = "UISlideOverlays";
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View) { fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View): SlideUpMenuOverlay {
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views); var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
menu.onOK.subscribe { menu.onOK.subscribe {
@@ -49,9 +71,10 @@ class UISlideOverlays {
onOk.invoke(); onOk.invoke();
}; };
menu.show(); menu.show();
return menu;
} }
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) { fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
val originalNotif = subscription.doNotifications; val originalNotif = subscription.doNotifications;
@@ -60,19 +83,48 @@ class UISlideOverlays {
val originalVideo = subscription.doFetchVideos; val originalVideo = subscription.doFetchVideos;
val originalPosts = subscription.doFetchPosts; val originalPosts = subscription.doFetchPosts;
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){ StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url); val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities(); val capabilities = plugin.getChannelCapabilities();
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
var menu: SlideUpMenuOverlay? = null;
items.addAll(listOf( items.addAll(listOf(
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", { SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications; subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
}, false), }, false),
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuGroup(container.context, "Subscription Groups",
"You can select which groups this subscription is part of.",
-1, listOf()) else null,
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuRecycler(container.context, "as") {
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
if(it is SubscriptionGroup.Selectable) {
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
?: return@subscribe;
groups.clear();
if(it.selected)
actualGroup.urls.remove(subscription.channel.url);
else
actualGroup.urls.add(subscription.channel.url);
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
adapter?.notifyContentChanged();
}
}
};
return@SlideUpMenuRecycler adapter;
} else null,
SlideUpMenuGroup(container.context, "Fetch Settings", SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.", "Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()), -1, listOf()),
@@ -91,9 +143,17 @@ class UISlideOverlays {
}, false) else null, }, false) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", { if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts; subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
}, false) else null).filterNotNull()); }, false) else null/*,,
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items); SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription",
-1, listOf())
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
showCreateSubscriptionGroup(container, subscription.channel);
}, false)*/
).filterNotNull());
menu.setItems(items);
if(subscription.doNotifications) if(subscription.doNotifications)
menu.selectOption(null, "notifications", true, true); menu.selectOption(null, "notifications", true, true);
@@ -110,8 +170,29 @@ class UISlideOverlays {
subscription.save(); subscription.save();
menu.hide(true); menu.hide(true);
if(subscription.doNotifications && !originalNotif && Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) { if(subscription.doNotifications && !originalNotif) {
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work"); val mainContext = StateApp.instance.contextOrNull;
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
if(mainContext is MainActivity) {
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
"You need to set a Background Updating interval for notifications", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Configure", {
val intent = Intent(mainContext, SettingsActivity::class.java);
intent.putExtra("query", mainContext.getString(R.string.background_update));
mainContext.startActivity(intent);
}, UIDialogs.ActionStyle.PRIMARY));
}
return@subscribe;
}
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
UIDialogs.toast(container.context, "Android notifications are disabled");
if(mainContext is MainActivity) {
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
}
}
} }
}; };
menu.onCancel.subscribe { menu.onCancel.subscribe {
@@ -127,6 +208,12 @@ class UISlideOverlays {
menu.show(); menu.show();
} }
} }
return menu;
}
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
} }
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay { fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
@@ -242,7 +329,7 @@ class UISlideOverlays {
val audioSources = if(descriptor is VideoUnMuxedSourceDescriptor) descriptor.audioSources else null; val audioSources = if(descriptor is VideoUnMuxedSourceDescriptor) descriptor.audioSources else null;
val subtitleSources = video.subtitles; val subtitleSources = video.subtitles;
if(videoSources.size == 0 && (audioSources?.size ?: 0) == 0) { if(videoSources.isEmpty() && (audioSources?.size ?: 0) == 0) {
UIDialogs.toast(container.context.getString(R.string.no_downloads_available), false); UIDialogs.toast(container.context.getString(R.string.no_downloads_available), false);
return null; return null;
} }
@@ -263,56 +350,62 @@ class UISlideOverlays {
videoSources videoSources
.filter { it.isDownloadable() } .filter { it.isDownloadable() }
.map { .map {
if (it is IVideoUrlSource) { when (it) {
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, { is IVideoUrlSource -> {
selectedVideo = it SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
menu?.selectOption(videoSources, it); selectedVideo = it
if(selectedAudio != null || !requiresAudio) menu?.selectOption(videoSources, it);
menu?.setOk(container.context.getString(R.string.download)); if(selectedAudio != null || !requiresAudio)
}, false) menu?.setOk(container.context.getString(R.string.download));
} else if (it is IHLSManifestSource) { }, false)
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, { }
showHlsPicker(video, it, it.url, container)
}, false) is IHLSManifestSource -> {
} else { SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
throw Exception("Unhandled source type") showHlsPicker(video, it, it.url, container)
}, false)
}
else -> {
throw Exception("Unhandled source type")
}
} }
}).flatten().toList() }).flatten().toList()
)); ));
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0) { if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
//TODO: Add HLS support here //TODO: Add HLS support here
selectedVideo = VideoHelper.selectBestVideoSource( selectedVideo = VideoHelper.selectBestVideoSource(
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(), videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
Settings.instance.downloads.getDefaultVideoQualityPixels(), Settings.instance.downloads.getDefaultVideoQualityPixels(),
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
) as IVideoUrlSource; ) as IVideoUrlSource?;
} }
audioSources?.let { audioSources -> if (audioSources != null) {
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
.filter { VideoHelper.isDownloadable(it) } .filter { VideoHelper.isDownloadable(it) }
.map { .map {
if (it is IAudioUrlSource) { when (it) {
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, { is IAudioUrlSource -> {
selectedAudio = it SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
menu?.selectOption(audioSources, it); selectedAudio = it
menu?.setOk(container.context.getString(R.string.download)); menu?.selectOption(audioSources, it);
}, false); menu?.setOk(container.context.getString(R.string.download));
} else if (it is IHLSManifestAudioSource) { }, false);
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, { }
showHlsPicker(video, it, it.url, container)
}, false) is IHLSManifestAudioSource -> {
} else { SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
throw Exception("Unhandled source type") showHlsPicker(video, it, it.url, container)
}, false)
}
else -> {
throw Exception("Unhandled source type")
}
} }
})); }));
val asources = audioSources;
val preferredAudioSource = VideoHelper.selectBestAudioSource(asources.asIterable(),
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
Settings.instance.playback.getPrimaryLanguage(container.context),
if(Settings.instance.downloads.isHighBitrateDefault()) 99999999 else 1);
menu?.selectOption(asources, preferredAudioSource);
//TODO: Add HLS support here //TODO: Add HLS support here
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(), selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
@@ -321,10 +414,8 @@ class UISlideOverlays {
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?; if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
} }
//ContentResolver is required for subtitles..
if(contentResolver != null && subtitleSources.isNotEmpty()) { if(contentResolver != null && subtitleSources.isNotEmpty()) {
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
.map {
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, { SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
if (selectedSubtitle == it) { if (selectedSubtitle == it) {
selectedSubtitle = null; selectedSubtitle = null;
@@ -334,7 +425,8 @@ class UISlideOverlays {
menu?.selectOption(subtitleSources, it); menu?.selectOption(subtitleSources, it);
} }
}, false); }, false);
})); })
);
} }
menu = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items); menu = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items);
@@ -418,10 +510,15 @@ class UISlideOverlays {
} }
} }
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) { fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate -> showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
StateDownloads.instance.download(playlist, px, bitrate); StateDownloads.instance.download(playlist, px, bitrate);
}; };
} }
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
StateDownloads.instance.downloadWatchLater(px, bitrate);
})
}
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) { private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
var menu: SlideUpMenuOverlay? = null; var menu: SlideUpMenuOverlay? = null;
@@ -502,6 +599,48 @@ class UISlideOverlays {
return overlay; return overlay;
} }
fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel? = null, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addSubGroupOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_subgroup), container.context.getString(R.string.ok), false, nameInput);
addSubGroupOverlay.onOK.subscribe {
val text = nameInput.text;
if (text.isBlank()) {
return@subscribe;
}
addSubGroupOverlay.hide();
nameInput.deactivate();
nameInput.clear();
if(onCreate == null)
{
//TODO: Do this better, temp
StateApp.instance.contextOrNull?.let {
if(it is MainActivity) {
val subGroup = SubscriptionGroup(text);
if(initialChannel != null) {
subGroup.urls.add(initialChannel.url);
if(initialChannel.thumbnail != null)
subGroup.image = ImageVariable(initialChannel.thumbnail);
}
it.navigate(it.getFragment<SubscriptionGroupFragment>(), subGroup);
}
}
}
else
onCreate(text)
};
addSubGroupOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addSubGroupOverlay.show();
nameInput.activate();
return addSubGroupOverlay
}
fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay { fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name)); val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput); val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
@@ -549,9 +688,17 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions", items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf( (listOf(
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), { SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
showDownloadVideoOverlay(video, container, true); showDownloadVideoOverlay(video, container, true);
}, false), }, false),
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND;
putExtra(Intent.EXTRA_TEXT, url);
type = "text/plain";
}, null));
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", { SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
StateMeta.instance.addHiddenCreator(video.author.url); StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home"); UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
@@ -589,7 +736,7 @@ class UISlideOverlays {
} }
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup): SlideUpMenuOverlay { fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup, slideUpMenuOverlayUpdated: (SlideUpMenuOverlay) -> Unit): SlideUpMenuOverlay {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
@@ -620,6 +767,13 @@ class UISlideOverlays {
); );
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
});
}, false))
for (playlist in allPlaylists) { for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "", playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
{ {
@@ -634,21 +788,22 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() }; return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
} }
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters { fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues); val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
overlay.show(); overlay.show();
return overlay; return overlay;
} }
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay { fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), invokeParents: Boolean = true, onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) }; val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) }; val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
val views = arrayOf(hidden val views = arrayOf(
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", { hidden
btn.handler?.invoke(btn); .map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
}, true) as View }.toTypedArray() ?: arrayOf(), btn.handler?.invoke(btn);
}, invokeParents) as View }.toTypedArray(),
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", { arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) { showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
val selected = it val selected = it
@@ -143,6 +143,7 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
} }
} }
@Suppress("DEPRECATION")
fun Activity.setNavigationBarColorAndIcons() { fun Activity.setNavigationBarColorAndIcons() {
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black); window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
@@ -5,13 +5,21 @@ import android.content.Intent
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.* import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
@@ -30,8 +38,10 @@ class AddSourceActivity : AppCompatActivity() {
private lateinit var _sourceHeader: SourceHeaderView; private lateinit var _sourceHeader: SourceHeaderView;
private lateinit var _sourcePermissions: LinearLayout; private lateinit var _sourcePermissions: LinearLayout;
private lateinit var _sourceWarnings: LinearLayout; private lateinit var _sourceWarnings: LinearLayout;
private lateinit var _sourceWarningsContainer: LinearLayout;
private lateinit var _container: ScrollView; private lateinit var _container: ScrollView;
private lateinit var _loader: ImageView; private lateinit var _loader: ImageView;
@@ -72,6 +82,7 @@ class AddSourceActivity : AppCompatActivity() {
_sourcePermissions = findViewById(R.id.source_permissions); _sourcePermissions = findViewById(R.id.source_permissions);
_sourceWarnings = findViewById(R.id.source_warnings); _sourceWarnings = findViewById(R.id.source_warnings);
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
_container = findViewById(R.id.configContainer); _container = findViewById(R.id.configContainer);
_loader = findViewById(R.id.loader); _loader = findViewById(R.id.loader);
@@ -194,23 +205,32 @@ class AddSourceActivity : AppCompatActivity() {
config.allowUrls, true) config.allowUrls, true)
) )
val pastelRed = resources.getColor(R.color.pastel_red); val pastelRed = ContextCompat.getColor(this, R.color.pastel_red);
for(warning in config.getWarnings(script)) val warnings = config.getWarnings(script);
for(warning in warnings)
_sourceWarnings.addView( _sourceWarnings.addView(
SourceInfoView(this, SourceInfoView(this,
R.drawable.ic_security_pred, R.drawable.ic_security_pred,
warning.first, warning.first,
warning.second) warning.second)
.withDescriptionColor(pastelRed)); .withDescriptionColor(pastelRed));
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
setLoading(false); setLoading(false);
} }
fun install(config: SourcePluginConfig, script: String) { fun install(config: SourcePluginConfig, script: String) {
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) { StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
if(it) if(it) {
StatePlatform.instance.clearUpdateAvailable(config)
if(isNew)
lifecycleScope.launch {
StatePlatform.instance.enableClient(listOf(config.id));
}
backToSources(); backToSources();
}
} }
} }
@@ -11,12 +11,12 @@ import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import com.journeyapps.barcodescanner.CaptureActivity
class AddSourceOptionsActivity : AppCompatActivity() { class AddSourceOptionsActivity : AppCompatActivity() {
lateinit var _buttonBack: ImageButton; lateinit var _buttonBack: ImageButton;
lateinit var _buttonQR: BigButton; lateinit var _buttonQR: BigButton;
lateinit var _buttonBrowse: BigButton;
lateinit var _buttonURL: BigButton; lateinit var _buttonURL: BigButton;
lateinit var _buttonPlugins: BigButton; lateinit var _buttonPlugins: BigButton;
@@ -57,6 +57,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
_buttonBack = findViewById(R.id.button_back); _buttonBack = findViewById(R.id.button_back);
_buttonQR = findViewById(R.id.option_qr); _buttonQR = findViewById(R.id.option_qr);
_buttonBrowse = findViewById(R.id.option_browse);
_buttonURL = findViewById(R.id.option_url); _buttonURL = findViewById(R.id.option_url);
_buttonPlugins = findViewById(R.id.option_plugins); _buttonPlugins = findViewById(R.id.option_plugins);
@@ -75,6 +76,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
integrator.setCaptureActivity(QRCaptureActivity::class.java); integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent()) _qrCodeResultLauncher.launch(integrator.createScanIntent())
} }
_buttonBrowse.onClick.subscribe {
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
}
_buttonURL.onClick.subscribe { _buttonURL.onClick.subscribe {
UIDialogs.toast(this, getString(R.string.not_implemented_yet)); UIDialogs.toast(this, getString(R.string.not_implemented_yet));
@@ -26,7 +26,7 @@ class DeveloperActivity : AppCompatActivity() {
_form = findViewById(R.id.settings_form); _form = findViewById(R.id.settings_form);
_form.fromObject(SettingsDev.instance); _form.fromObject(SettingsDev.instance);
_form.onChanged.subscribe { field, value -> _form.onChanged.subscribe { _, _ ->
_form.setObjectValues(); _form.setObjectValues();
SettingsDev.instance.save(); SettingsDev.instance.save();
}; };
@@ -4,15 +4,19 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.LogLevel import com.futo.platformplayer.logging.LogLevel
import com.futo.platformplayer.logging.Logging import com.futo.platformplayer.logging.Logging
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateUpdate
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -26,6 +30,7 @@ class ExceptionActivity : AppCompatActivity() {
private lateinit var _buttonSubmit: LinearLayout; private lateinit var _buttonSubmit: LinearLayout;
private lateinit var _buttonRestart: LinearLayout; private lateinit var _buttonRestart: LinearLayout;
private lateinit var _buttonClose: LinearLayout; private lateinit var _buttonClose: LinearLayout;
private lateinit var _buttonCheckForUpdates: LinearLayout;
private var _file: File? = null; private var _file: File? = null;
private var _submitted = false; private var _submitted = false;
@@ -43,6 +48,7 @@ class ExceptionActivity : AppCompatActivity() {
_buttonSubmit = findViewById(R.id.button_submit); _buttonSubmit = findViewById(R.id.button_submit);
_buttonRestart = findViewById(R.id.button_restart); _buttonRestart = findViewById(R.id.button_restart);
_buttonClose = findViewById(R.id.button_close); _buttonClose = findViewById(R.id.button_close);
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context); val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace); val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
@@ -81,6 +87,17 @@ class ExceptionActivity : AppCompatActivity() {
_buttonClose.setOnClickListener { _buttonClose.setOnClickListener {
finish(); finish();
}; };
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
_buttonCheckForUpdates.visibility = View.VISIBLE
_buttonCheckForUpdates.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(this@ExceptionActivity, true, true)
}
}
} else {
_buttonCheckForUpdates.visibility = View.GONE
}
} }
private fun submitFile() { private fun submitFile() {
@@ -2,7 +2,6 @@ package com.futo.platformplayer.activities
import android.content.Intent import android.content.Intent
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
interface IWithResultLauncher { interface IWithResultLauncher {
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit); fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);
@@ -3,24 +3,23 @@ package com.futo.platformplayer.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.webkit.ConsoleMessage
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.LoginWebViewClient import com.futo.platformplayer.others.LoginWebViewClient
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -41,6 +40,7 @@ class LoginActivity : AppCompatActivity() {
_textUrl = findViewById(R.id.text_url); _textUrl = findViewById(R.id.text_url);
_buttonClose = findViewById(R.id.button_close); _buttonClose = findViewById(R.id.button_close);
_buttonClose.setOnClickListener { _buttonClose.setOnClickListener {
UIDialogs.toast("Login cancelled", false);
finish(); finish();
} }
@@ -102,7 +102,7 @@ class LoginActivity : AppCompatActivity() {
override fun finish() { override fun finish() {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
_webView?.loadUrl("about:blank"); _webView.loadUrl("about:blank");
} }
_callback?.let { _callback?.let {
_callback = null; _callback = null;
@@ -1,9 +1,11 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@@ -16,17 +18,20 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dialogs.ConnectCastingDialog
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.fragment.mainactivity.main.* import com.futo.platformplayer.fragment.mainactivity.main.*
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
@@ -36,21 +41,24 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.listeners.OrientationManager import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.* import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SubscriptionStorage import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import com.futo.polycentric.core.ApiMethods
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class MainActivity : AppCompatActivity, IWithResultLauncher { class MainActivity : AppCompatActivity, IWithResultLauncher {
@@ -62,6 +70,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var rootView : MotionLayout; lateinit var rootView : MotionLayout;
private lateinit var _overlayContainer: FrameLayout; private lateinit var _overlayContainer: FrameLayout;
private lateinit var _toastView: ToastView;
//Segment Containers //Segment Containers
private lateinit var _fragContainerTopBar: FragmentContainerView; private lateinit var _fragContainerTopBar: FragmentContainerView;
@@ -92,6 +101,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment; lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment;
lateinit var _fragMainChannel: ChannelFragment; lateinit var _fragMainChannel: ChannelFragment;
lateinit var _fragMainSources: SourcesFragment; lateinit var _fragMainSources: SourcesFragment;
lateinit var _fragMainTutorial: TutorialFragment;
lateinit var _fragMainPlaylists: PlaylistsFragment; lateinit var _fragMainPlaylists: PlaylistsFragment;
lateinit var _fragMainPlaylist: PlaylistFragment; lateinit var _fragMainPlaylist: PlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment; lateinit var _fragWatchlist: WatchLaterFragment;
@@ -101,6 +111,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment; lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
lateinit var _fragImportPlaylists: ImportPlaylistsFragment; lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
lateinit var _fragBuy: BuyFragment; lateinit var _fragBuy: BuyFragment;
lateinit var _fragSubGroup: SubscriptionGroupFragment;
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
lateinit var _fragBrowser: BrowserFragment; lateinit var _fragBrowser: BrowserFragment;
@@ -132,7 +144,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
try { try {
handleUrlAll(content) runBlocking {
handleUrlAll(content)
}
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.i(TAG, "Failed to handle URL.", e) Logger.i(TAG, "Failed to handle URL.", e)
UIDialogs.toast(this, "Failed to handle URL: ${e.message}") UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
@@ -141,6 +155,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
constructor() : super() { constructor() : super() {
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
Thread.setDefaultUncaughtExceptionHandler { _, throwable -> Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
val writer = StringWriter(); val writer = StringWriter();
@@ -179,6 +195,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Logger.i(TAG, "MainActivity Starting");
StateApp.instance.setGlobalContext(this, lifecycleScope); StateApp.instance.setGlobalContext(this, lifecycleScope);
StateApp.instance.mainAppStarting(this); StateApp.instance.mainAppStarting(this);
@@ -201,7 +218,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay); _fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container); _fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
_overlayContainer = findViewById(R.id.overlay_container); _overlayContainer = findViewById(R.id.overlay_container);
//_overlayContainer.visibility = View.GONE; _toastView = findViewById(R.id.toast_view);
//Initialize fragments //Initialize fragments
@@ -217,6 +234,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//Main //Main
_fragMainHome = HomeFragment.newInstance(); _fragMainHome = HomeFragment.newInstance();
_fragMainTutorial = TutorialFragment.newInstance()
_fragMainSuggestions = SuggestionsFragment.newInstance(); _fragMainSuggestions = SuggestionsFragment.newInstance();
_fragMainVideoSearchResults = ContentSearchResultsFragment.newInstance(); _fragMainVideoSearchResults = ContentSearchResultsFragment.newInstance();
_fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance(); _fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
@@ -236,6 +254,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance(); _fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
_fragImportPlaylists = ImportPlaylistsFragment.newInstance(); _fragImportPlaylists = ImportPlaylistsFragment.newInstance();
_fragBuy = BuyFragment.newInstance(); _fragBuy = BuyFragment.newInstance();
_fragSubGroup = SubscriptionGroupFragment.newInstance();
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
_fragBrowser = BrowserFragment.newInstance(); _fragBrowser = BrowserFragment.newInstance();
@@ -306,6 +326,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainCreatorSearchResults.topBar = _fragTopBarSearch; _fragMainCreatorSearchResults.topBar = _fragTopBarSearch;
_fragMainPlaylistSearchResults.topBar = _fragTopBarSearch; _fragMainPlaylistSearchResults.topBar = _fragTopBarSearch;
_fragMainChannel.topBar = _fragTopBarNavigation; _fragMainChannel.topBar = _fragTopBarNavigation;
_fragMainTutorial.topBar = _fragTopBarNavigation;
_fragMainSubscriptionsFeed.topBar = _fragTopBarGeneral; _fragMainSubscriptionsFeed.topBar = _fragTopBarGeneral;
_fragMainSources.topBar = _fragTopBarAdd; _fragMainSources.topBar = _fragTopBarAdd;
_fragMainPlaylists.topBar = _fragTopBarGeneral; _fragMainPlaylists.topBar = _fragTopBarGeneral;
@@ -317,9 +338,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragDownloads.topBar = _fragTopBarGeneral; _fragDownloads.topBar = _fragTopBarGeneral;
_fragImportSubscriptions.topBar = _fragTopBarImport; _fragImportSubscriptions.topBar = _fragTopBarImport;
_fragImportPlaylists.topBar = _fragTopBarImport; _fragImportPlaylists.topBar = _fragTopBarImport;
_fragSubGroupList.topBar = _fragTopBarAdd;
_fragBrowser.topBar = _fragTopBarNavigation; _fragBrowser.topBar = _fragTopBarNavigation;
fragCurrent = _fragMainHome; fragCurrent = _fragMainHome;
val defaultTab = Settings.instance.tabs.mapNotNull { val defaultTab = Settings.instance.tabs.mapNotNull {
@@ -401,6 +423,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
StateApp.instance.mainAppStartedWithExternalFiles(this); StateApp.instance.mainAppStartedWithExternalFiles(this);
//startActivity(Intent(this, TestActivity::class.java)); //startActivity(Intent(this, TestActivity::class.java));
val sharedPreferences = getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
if (isFirstBoot) {
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
navigate(_fragMainTutorial)
})
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
}
} }
@@ -457,21 +489,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
_isVisible = true; _isVisible = true;
val videoToOpen = StateSaved.instance.videoToOpen;
if (_wasStopped) {
_wasStopped = false;
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
_fragVideoDetail.maximizeVideoDetail(true);
}
StateSaved.instance.setVideoToOpenNonBlocking(null);
}
}
} }
override fun onPause() { override fun onPause() {
@@ -526,13 +543,28 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
navigate(_fragMainSources); navigate(_fragMainSources);
} }
}; };
"BROWSE_PLUGINS" -> {
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let {
if(it is MainActivity) {
runBlocking {
it.handleUrlAll(req.url.toString());
}
}
};
}
)));
}
} }
} }
} }
try { try {
if (targetData != null) { if (targetData != null) {
handleUrlAll(targetData) runBlocking {
handleUrlAll(targetData)
}
} }
} }
catch(ex: Throwable) { catch(ex: Throwable) {
@@ -540,7 +572,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
fun handleUrlAll(url: String) { suspend fun handleUrlAll(url: String) {
val uri = Uri.parse(url) val uri = Uri.parse(url)
when (uri.scheme) { when (uri.scheme) {
"grayjay" -> { "grayjay" -> {
@@ -576,7 +608,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
this, this,
R.drawable.ic_play, R.drawable.ic_play,
getString(R.string.unknown_content_format) + " [${url}]", getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
"Ok", "Ok",
{ }); { });
} }
@@ -624,23 +656,38 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
fun handleUrl(url: String): Boolean { suspend fun handleUrl(url: String): Boolean {
Logger.i(TAG, "handleUrl(url=$url)") Logger.i(TAG, "handleUrl(url=$url)")
if (StatePlatform.instance.hasEnabledVideoClient(url)) { return withContext(Dispatchers.IO) {
navigate(_fragVideoDetail, url); Logger.i(TAG, "handleUrl(url=$url) on IO");
_fragVideoDetail.maximizeVideoDetail(true); if (StatePlatform.instance.hasEnabledVideoClient(url)) {
return true; Logger.i(TAG, "handleUrl(url=$url) found video client");
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) { lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainChannel, url); navigate(_fragVideoDetail, url);
lifecycleScope.launch { _fragVideoDetail.maximizeVideoDetail(true);
delay(100); }
_fragVideoDetail.minimizeVideoDetail(); return@withContext true;
}; } else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
return true; Logger.i(TAG, "handleUrl(url=$url) found channel client");
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainChannel, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return@withContext true;
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainPlaylist, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return@withContext true;
}
return@withContext false;
} }
return false;
} }
fun handleContent(file: String, mime: String? = null): Boolean { fun handleContent(file: String, mime: String? = null): Boolean {
Logger.i(TAG, "handleContent(url=$file)"); Logger.i(TAG, "handleContent(url=$file)");
@@ -649,12 +696,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(file.lowercase().endsWith(".json") || mime == "application/json") { if(file.lowercase().endsWith(".json") || mime == "application/json") {
var recon = String(data); var recon = String(data);
if(!recon.trim().startsWith("[")) if(!recon.trim().startsWith("["))
return handleUnknownJson(file, recon); return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
val reconLines = Json.decodeFromString<List<String>>(recon);
recon = reconLines.joinToString("\n"); recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon); handleReconstruction(recon, cache);
return true; return true;
} }
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") { else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
@@ -669,12 +728,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun handleFile(file: String): Boolean { fun handleFile(file: String): Boolean {
Logger.i(TAG, "handleFile(url=$file)"); Logger.i(TAG, "handleFile(url=$file)");
if(file.lowercase().endsWith(".json")) { if(file.lowercase().endsWith(".json")) {
val recon = String(readSharedFile(file)); var recon = String(readSharedFile(file));
if(!recon.startsWith("[")) if(!recon.startsWith("["))
return handleUnknownJson(file, recon); return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon); handleReconstruction(recon, cache);
return true; return true;
} }
else if(file.lowercase().endsWith(".zip")) { else if(file.lowercase().endsWith(".zip")) {
@@ -686,7 +758,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
return false; return false;
} }
fun handleReconstruction(recon: String) { fun handleReconstruction(recon: String, cache: ImportCache? = null) {
val type = ManagedStore.getReconstructionIdentifier(recon); val type = ManagedStore.getReconstructionIdentifier(recon);
val store: ManagedStore<*> = when(type) { val store: ManagedStore<*> = when(type) {
"Playlist" -> StatePlaylists.instance.playlistStore "Playlist" -> StatePlaylists.instance.playlistStore
@@ -703,7 +775,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!type.isNullOrEmpty()) { if(!type.isNullOrEmpty()) {
UIDialogs.showImportDialog(this, store, name, listOf(recon)) { UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
} }
} }
@@ -723,7 +795,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
return false; return false;
} }
fun handleUnknownJson(name: String?, json: String): Boolean { fun handleUnknownJson(json: String): Boolean {
val context = this; val context = this;
@@ -793,11 +865,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(_fragBotBarMenu.onBackPressed()) if(_fragBotBarMenu.onBackPressed())
return; return;
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
_fragVideoDetail.onBackPressed())
return; return;
if(!fragCurrent.onBackPressed()) if(!fragCurrent.onBackPressed())
closeSegment(); closeSegment();
} }
@@ -832,7 +902,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED; val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop") Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
_fragVideoDetail?.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig); _fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
Logger.v(TAG, "onPictureInPictureModeChanged Ready"); Logger.v(TAG, "onPictureInPictureModeChanged Ready");
} }
@@ -843,7 +913,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_orientationManager.disable(); _orientationManager.disable();
StateApp.instance.mainAppDestroyed(this); StateApp.instance.mainAppDestroyed(this);
StateSaved.instance.setVideoToOpenBlocking(null);
} }
inline fun <reified T> isFragmentActive(): Boolean { inline fun <reified T> isFragmentActive(): Boolean {
@@ -854,6 +923,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
* Navigate takes a MainFragment, and makes them the current main visible view * Navigate takes a MainFragment, and makes them the current main visible view
* A parameter can be provided which becomes available in the onShow of said fragment * A parameter can be provided which becomes available in the onShow of said fragment
*/ */
@SuppressLint("CommitTransaction")
fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) { fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)") Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)")
@@ -889,7 +959,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
transaction = transaction.replace(R.id.fragment_main, segment); transaction = transaction.replace(R.id.fragment_main, segment);
val extraBottomDP = if(_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED) HEIGHT_VIDEO_MINIMIZED_DP else 0f
if (segment.hasBottomBar) { if (segment.hasBottomBar) {
if (!fragCurrent.hasBottomBar) if (!fragCurrent.hasBottomBar)
transaction = transaction.show(_fragBotBarMenu); transaction = transaction.show(_fragBotBarMenu);
@@ -899,13 +968,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
transaction = transaction.hide(_fragBotBarMenu); transaction = transaction.hide(_fragBotBarMenu);
} }
transaction.commitNow(); transaction.commitNow();
} } else {
else {
//Special cases
if(segment is VideoDetailFragment) {
_fragContainerVideoDetail.visibility = View.VISIBLE;
_fragVideoDetail.maximizeVideoDetail();
}
if(!segment.hasBottomBar) { if(!segment.hasBottomBar) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
@@ -965,6 +1028,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
inline fun <reified T : Fragment> getFragment() : T { inline fun <reified T : Fragment> getFragment() : T {
return when(T::class) { return when(T::class) {
HomeFragment::class -> _fragMainHome as T; HomeFragment::class -> _fragMainHome as T;
TutorialFragment::class -> _fragMainTutorial as T;
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T; ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
CreatorSearchResultsFragment::class -> _fragMainCreatorSearchResults as T; CreatorSearchResultsFragment::class -> _fragMainCreatorSearchResults as T;
SuggestionsFragment::class -> _fragMainSuggestions as T; SuggestionsFragment::class -> _fragMainSuggestions as T;
@@ -989,6 +1053,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
ImportPlaylistsFragment::class -> _fragImportPlaylists as T; ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
BrowserFragment::class -> _fragBrowser as T; BrowserFragment::class -> _fragBrowser as T;
BuyFragment::class -> _fragBuy as T; BuyFragment::class -> _fragBuy as T;
SubscriptionGroupFragment::class -> _fragSubGroup as T;
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity"); else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
} }
} }
@@ -1008,6 +1074,70 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
fun requestNotificationPermissions(reason: String) {
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
reason, null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
private val _toastQueue = ConcurrentLinkedQueue<ToastView.Toast>();
private var _toastJob: Job? = null;
fun showAppToast(toast: ToastView.Toast) {
synchronized(_toastQueue) {
_toastQueue.add(toast);
if(_toastJob?.isActive != true)
_toastJob = lifecycleScope.launch(Dispatchers.Default) {
launchAppToastJob();
};
}
}
private suspend fun launchAppToastJob() {
Logger.i(TAG, "Starting appToast loop");
while(!_toastQueue.isEmpty()) {
val toast = _toastQueue.poll() ?: continue;
Logger.i(TAG, "Showing next toast (${toast.msg})");
lifecycleScope.launch(Dispatchers.Main) {
if (!_toastView.isVisible) {
Logger.i(TAG, "First showing toast");
_toastView.setToast(toast);
_toastView.show(true);
} else {
_toastView.setToastAnimated(toast);
}
}
if(toast.long)
delay(5000);
else
delay(3000);
}
Logger.i(TAG, "Ending appToast loop");
lifecycleScope.launch(Dispatchers.Main) {
_toastView.hide(true) {
};
}
}
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers. //TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>(); private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
@@ -55,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
Settings.instance.save() Settings.instance.save()
} }
val items = Settings.instance.tabs.mapNotNull { val items = ArrayList(Settings.instance.tabs.mapNotNull {
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
TabViewHolderData(buttonDefinition, it.enabled) TabViewHolderData(buttonDefinition, it.enabled)
}; });
_listTabs = _recyclerTabs.asAny(items) { _listTabs = _recyclerTabs.asAny(items) {
it.onDragDrop.subscribe { vh -> it.onDragDrop.subscribe { vh ->
@@ -12,14 +12,13 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.Synchronization
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -71,6 +70,13 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
try { try {
processHandle = ProcessHandle.create(); processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret); Store.instance.addProcessSecret(processHandle.processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer("https://srv1-stg.polycentric.io"); processHandle.addServer("https://srv1-stg.polycentric.io");
processHandle.setUsername(username); processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle); StatePolycentric.instance.setProcessHandle(processHandle);
@@ -12,14 +12,20 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.polycentric.core.* import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret
import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.base64UrlToByteArray
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import com.journeyapps.barcodescanner.CaptureActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
@@ -30,6 +36,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
private lateinit var _buttonScanProfile: LinearLayout; private lateinit var _buttonScanProfile: LinearLayout;
private lateinit var _buttonImportProfile: LinearLayout; private lateinit var _buttonImportProfile: LinearLayout;
private lateinit var _editProfile: EditText; private lateinit var _editProfile: EditText;
private lateinit var _loaderOverlay: LoaderOverlay;
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data) val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@@ -53,6 +60,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help); _buttonHelp = findViewById(R.id.button_help);
_buttonScanProfile = findViewById(R.id.button_scan_profile); _buttonScanProfile = findViewById(R.id.button_scan_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile); _buttonImportProfile = findViewById(R.id.button_import_profile);
_loaderOverlay = findViewById(R.id.loader_overlay);
_editProfile = findViewById(R.id.edit_profile); _editProfile = findViewById(R.id.edit_profile);
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
@@ -95,42 +103,63 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
return; return;
} }
try { _loaderOverlay.show()
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
val exportBundle = ExportBundle.parseFrom(urlInfo.body); lifecycleScope.launch(Dispatchers.IO) {
val keyPair = KeyPair.fromProto(exportBundle.keyPair); try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey); val exportBundle = ExportBundle.parseFrom(urlInfo.body);
if (existingProcessSecret != null) { val keyPair = KeyPair.fromProto(exportBundle.keyPair);
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
return;
}
val processSecret = ProcessSecret(keyPair, Process.random()); val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
Store.instance.addProcessSecret(processSecret); if (existingProcessSecret != null) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
}
return@launch;
}
val processHandle = processSecret.toProcessHandle(); val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret);
for (e in exportBundle.events.eventsList) {
try { try {
val se = SignedEvent.fromProto(e); PolycentricStorage.instance.addProcessSecret(processSecret)
Store.instance.putSignedEvent(se);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Ignored invalid event", e); Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) {
try {
val se = SignedEvent.fromProto(e);
Store.instance.putSignedEvent(se);
} catch (e: Throwable) {
Logger.w(TAG, "Ignored invalid event", e);
}
}
StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
}
} catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e);
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
}
} finally {
withContext(Dispatchers.Main) {
_loaderOverlay.hide();
} }
} }
StatePolycentric.instance.setProcessHandle(processHandle);
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
} catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e);
UIDialogs.toast(this, getString(R.string.failed_to_import_profile) + " '${e.message}'");
} }
} }
@@ -1,6 +1,8 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -12,25 +14,26 @@ import android.webkit.MimeTypeMap
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dialogs.CommentDialog
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.Synchronization
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.toURLInfoDataLink import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -48,6 +51,8 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonDelete: BigButton; private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String; private lateinit var _username: String;
private lateinit var _imagePolycentric: ImageView; private lateinit var _imagePolycentric: ImageView;
private lateinit var _loaderOverlay: LoaderOverlay;
private lateinit var _textSystem: TextView;
private var _avatarUri: Uri? = null; private var _avatarUri: Uri? = null;
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
@@ -65,28 +70,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
_buttonExport = findViewById(R.id.button_export); _buttonExport = findViewById(R.id.button_export);
_buttonLogout = findViewById(R.id.button_logout); _buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete); _buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay);
_textSystem = findViewById(R.id.text_system)
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
saveIfRequired(); saveIfRequired();
finish(); finish();
}; };
lifecycleScope.launch(Dispatchers.IO) {
try {
val processHandle = StatePolycentric.instance.processHandle!!;
Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io");
withContext(Dispatchers.Main) {
updateUI();
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
}
}
}
updateUI();
_imagePolycentric.setOnClickListener { _imagePolycentric.setOnClickListener {
ImagePicker.with(this) ImagePicker.with(this)
.cropSquare() .cropSquare()
@@ -122,6 +112,37 @@ class PolycentricProfileActivity : AppCompatActivity() {
finish(); finish();
}); });
} }
_textSystem.setOnLongClickListener {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("system", _textSystem.text)
clipboard.setPrimaryClip(clip)
return@setOnLongClickListener true
}
updateUI()
StatePolycentric.instance.processHandle?.let { processHandle ->
_loaderOverlay.show()
lifecycleScope.launch(Dispatchers.IO) {
try {
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
withContext(Dispatchers.Main) {
updateUI();
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
}
} finally {
withContext(Dispatchers.Main) {
_loaderOverlay.hide()
}
}
}
}
} }
private fun saveIfRequired() { private fun saveIfRequired() {
@@ -130,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() {
var hasChanges = false; var hasChanges = false;
val username = _editName.text.toString(); val username = _editName.text.toString();
if (username.length < 3) { if (username.length < 3) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long)); withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
}
return@launch; return@launch;
} }
val processHandle = StatePolycentric.instance.processHandle; val processHandle = StatePolycentric.instance.processHandle;
if (processHandle == null) { if (processHandle == null) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset)); withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
}
return@launch; return@launch;
} }
@@ -221,6 +246,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
private fun updateUI() { private fun updateUI() {
val processHandle = StatePolycentric.instance.processHandle!!; val processHandle = StatePolycentric.instance.processHandle!!;
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system)) val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
_textSystem.text = processHandle.system.key.toBase64Url()
_username = systemState.username; _username = systemState.username;
_editName.text.clear(); _editName.text.clear();
_editName.text.append(_username); _editName.text.append(_username);
@@ -250,7 +276,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
} }
private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? { private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? {
var mimeType: String? = null; var mimeType: String?;
// Try to get MIME type from the content URI // Try to get MIME type from the content URI
mimeType = contentResolver.getType(uri); mimeType = contentResolver.getType(uri);
@@ -1,8 +1,10 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
@@ -12,6 +14,8 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@@ -33,6 +37,14 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
lateinit var overlay: FrameLayout; lateinit var overlay: FrameLayout;
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext") Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -49,7 +61,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
_loaderView = findViewById(R.id.loader); _loaderView = findViewById(R.id.loader);
overlay = findViewById(R.id.overlay_container); overlay = findViewById(R.id.overlay_container);
_form.onChanged.subscribe { field, value -> _form.onChanged.subscribe { field, _ ->
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving"); Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
_form.setObjectValues(); _form.setObjectValues();
Settings.instance.save(); Settings.instance.save();
@@ -58,6 +70,33 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences"); Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString()); StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
} }
if(field.descriptor?.id == "background_update") {
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
if(!notifManager.areNotificationsEnabled()) {
UIDialogs.toast(this, "Notifications aren't enabled");
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
"Notifications need to be enabled for background updating to function", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
}
}
}; };
_buttonBack.setOnClickListener { _buttonBack.setOnClickListener {
finish(); finish();
@@ -72,7 +111,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
reloadSettings(); reloadSettings();
} }
var isFirstLoad = true;
fun reloadSettings() { fun reloadSettings() {
val firstLoad = isFirstLoad;
isFirstLoad = false;
_form.setSearchVisible(false); _form.setSearchVisible(false);
_loaderView.start(); _loaderView.start();
_form.fromObject(lifecycleScope, Settings.instance) { _form.fromObject(lifecycleScope, Settings.instance) {
@@ -90,6 +132,13 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode)); UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
} }
}; };
if(firstLoad) {
val query = intent.getStringExtra("query");
if(!query.isNullOrEmpty()) {
_form.setSearchQuery(query);
}
}
}; };
} }
@@ -135,6 +184,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
resultLauncher.launch(intent); resultLauncher.launch(intent);
} }
companion object { companion object {
//TODO: Temporary for solving Settings issues //TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@@ -13,8 +13,6 @@ import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import java.util.Dictionary
import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
open class ManagedHttpClient { open class ManagedHttpClient {
@@ -60,7 +58,7 @@ open class ManagedHttpClient {
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.url(url); .url(url);
if(user_agent != null && !user_agent.isEmpty() && !headers.any { it.key.lowercase() == "user-agent" }) if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
requestBuilder.addHeader("User-Agent", user_agent) requestBuilder.addHeader("User-Agent", user_agent)
for (pair in headers.entries) for (pair in headers.entries)
@@ -137,7 +135,7 @@ open class ManagedHttpClient {
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(request.method, requestBody) .method(request.method, requestBody)
.url(request.url); .url(request.url);
if(user_agent != null && !user_agent.isEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" }) if(user_agent.isNotEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" })
requestBuilder.addHeader("User-Agent", user_agent) requestBuilder.addHeader("User-Agent", user_agent)
for (pair in request.headers.entries) for (pair in request.headers.entries)
@@ -148,7 +146,7 @@ open class ManagedHttpClient {
val time = measureTimeMillis { val time = measureTimeMillis {
val call = client.newCall(requestBuilder.build()); val call = client.newCall(requestBuilder.build());
request.onCallCreated?.emit(call); request.onCallCreated.emit(call);
response = call.execute() response = call.execute()
resp = Response( resp = Response(
response.code, response.code,
@@ -1,11 +1,11 @@
package com.futo.platformplayer.api.http.server package com.futo.platformplayer.api.http.server
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
import com.futo.platformplayer.api.http.server.handlers.HttpHandler import com.futo.platformplayer.api.http.server.handlers.HttpHandler
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
import com.futo.platformplayer.logging.Logger
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.OutputStream import java.io.OutputStream
import java.lang.reflect.Field import java.lang.reflect.Field
@@ -14,11 +14,10 @@ import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.ServerSocket import java.net.ServerSocket
import java.net.Socket import java.net.Socket
import java.util.* import java.util.UUID
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.stream.IntStream.range import java.util.stream.IntStream.range
import kotlin.collections.HashMap
class ManagedHttpServer(private val _requestedPort: Int = 0) { class ManagedHttpServer(private val _requestedPort: Int = 0) {
private val _client : ManagedHttpClient = ManagedHttpClient(); private val _client : ManagedHttpClient = ManagedHttpClient();
@@ -192,7 +191,7 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
} }
} }
fun addBridgeHandlers(obj: Any, tag: String? = null) { fun addBridgeHandlers(obj: Any, tag: String? = null) {
val tagToUse = tag ?: obj.javaClass.name; //val tagToUse = tag ?: obj.javaClass.name;
val getMethods = obj::class.java.declaredMethods val getMethods = obj::class.java.declaredMethods
.filter { it.getAnnotation(HttpGET::class.java) != null } .filter { it.getAnnotation(HttpGET::class.java) != null }
.map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) } .map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) }
@@ -212,13 +211,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply { addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
if(!getMethod.second.contentType.isEmpty()) if(!getMethod.second.contentType.isEmpty())
this.withContentType(getMethod.second.contentType); this.withContentType(getMethod.second.contentType);
}.withContentType(getMethod.second.contentType ?: ""); }.withContentType(getMethod.second.contentType);
for(postMethod in postMethods) for(postMethod in postMethods)
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1) if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply { addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
if(!postMethod.second.contentType.isEmpty()) if(!postMethod.second.contentType.isEmpty())
this.withContentType(postMethod.second.contentType); this.withContentType(postMethod.second.contentType);
}.withContentType(postMethod.second.contentType ?: ""); }.withContentType(postMethod.second.contentType);
for(getField in getFields) { for(getField in getFields) {
getField.first.isAccessible = true; getField.first.isAccessible = true;
@@ -232,13 +231,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
} }
else else
it.respondCode(204); it.respondCode(204);
}).withContentType(getField.second.contentType ?: ""); }).withContentType(getField.second.contentType);
} }
} }
private fun keepAliveLoop(requestReader: BufferedInputStream, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) { private fun keepAliveLoop(requestReader: BufferedInputStream, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) {
val stopCount = _stopCount; val stopCount = _stopCount;
var keepAlive = false; var keepAlive: Boolean;
var requestsMax = 0; var requestsMax = 0;
var requestsTotal = 0; var requestsTotal = 0;
do { do {
@@ -288,11 +287,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
for (intf in NetworkInterface.getNetworkInterfaces()) { for (intf in NetworkInterface.getNetworkInterfaces()) {
for (addr in intf.inetAddresses) { for (addr in intf.inetAddresses) {
if (!addr.isLoopbackAddress) { if (!addr.isLoopbackAddress) {
val ipString: String = addr.hostAddress; val ipString: String = addr.hostAddress ?: continue
val isIPv4 = ipString.indexOf(':') < 0; val isIPv4 = ipString.indexOf(':') < 0
if (!isIPv4) if (!isIPv4) {
continue; continue
addresses.add(addr); }
addresses.add(addr)
} }
} }
} }
@@ -1,6 +1,3 @@
package com.futo.platformplayer.api.http.server.exceptions package com.futo.platformplayer.api.http.server.exceptions
import java.net.SocketTimeoutException
import java.util.concurrent.TimeoutException
class EmptyRequestException(msg: String) : Exception(msg) {} class EmptyRequestException(msg: String) : Exception(msg) {}
@@ -10,12 +10,9 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist
/** /**
* A temporary class that caches video results * A temporary class that caches video results
@@ -44,8 +41,7 @@ class CachedPlatformClient : IPlatformClient {
var result = _cache.get(url); var result = _cache.get(url);
if(result == null) { if(result == null) {
result = _client.getContentDetails(url); result = _client.getContentDetails(url);
if (result != null) _cache.put(url, result);
_cache.put(url, result);
} }
return result; return result;
} }
@@ -64,6 +60,9 @@ class CachedPlatformClient : IPlatformClient {
filters: Map<String, List<String>>? filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl); ): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues) override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query); override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
@@ -10,11 +10,9 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist
/** /**
* A client for a specific platform * A client for a specific platform
@@ -86,6 +84,15 @@ interface IPlatformClient {
*/ */
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>; fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
/**
* Describes what the plugin is capable on peek channel results
*/
fun getPeekChannelTypes(): List<String>;
/**
* Peeks contents of a channel, upload time descending
*/
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
/** /**
* Gets the channel url associated with a claimType * Gets the channel url associated with a claimType
*/ */
@@ -9,7 +9,6 @@ import com.caverock.androidsvg.SVG
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.live.LiveEventComment import com.futo.platformplayer.api.media.models.live.LiveEventComment
import com.futo.platformplayer.api.media.models.live.LiveEventDonation
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
@@ -195,7 +194,7 @@ class LiveChatManager {
fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) { fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) {
var drawable: Drawable? = null; var drawable: Drawable? = null;
var url: String? = null; var url: String?;
synchronized(_cache_lock) { synchronized(_cache_lock) {
url = _cache_urls[emoji]; url = _cache_urls[emoji];
if(url != null) if(url != null)
@@ -13,10 +13,12 @@ data class PlatformClientCapabilities(
val hasGetChannelUrlByClaim: Boolean = false, val hasGetChannelUrlByClaim: Boolean = false,
val hasGetChannelTemplateByClaimMap: Boolean = false, val hasGetChannelTemplateByClaimMap: Boolean = false,
val hasGetSearchCapabilities: Boolean = false, val hasGetSearchCapabilities: Boolean = false,
val hasGetSearchChannelContentsCapabilities: Boolean = false,
val hasGetChannelCapabilities: Boolean = false, val hasGetChannelCapabilities: Boolean = false,
val hasGetLiveEvents: Boolean = false, val hasGetLiveEvents: Boolean = false,
val hasGetLiveChatWindow: Boolean = false, val hasGetLiveChatWindow: Boolean = false,
val hasGetContentChapters: Boolean = false val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false
) { ) {
} }
@@ -20,7 +20,7 @@ class PlatformMultiClientPool {
val pool = synchronized(_clientPools) { val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient)) if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply { _clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
this.onDead.subscribe { client, pool -> this.onDead.subscribe { _, pool ->
synchronized(_clientPools) { synchronized(_clientPools) {
if(_clientPools[parentClient] == pool) if(_clientPools[parentClient] == pool)
_clientPools.remove(parentClient); _clientPools.remove(parentClient);
@@ -14,7 +14,7 @@ open class PlatformAuthorLink {
val id: PlatformID; val id: PlatformID;
val name: String; val name: String;
val url: String; val url: String;
val thumbnail: String?; var thumbnail: String?;
var subscribers: Long? = null; //Optional var subscribers: Long? = null; //Optional
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null) constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
@@ -64,7 +64,6 @@ class FilterGroup(
val isMultiSelect: Boolean, val isMultiSelect: Boolean,
val id: String? = null val id: String? = null
) { ) {
@kotlinx.serialization.Transient
val idOrName: String get() = id ?: name; val idOrName: String get() = id ?: name;
companion object { companion object {
@@ -3,6 +3,8 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@@ -31,7 +33,7 @@ class Thumbnails {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails")) return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray() .toArray()
.map { Thumbnail.fromV8(it as V8ValueObject) } .map { Thumbnail.fromV8(config, it as V8ValueObject) }
.toTypedArray()); .toTypedArray());
} }
} }
@@ -40,10 +42,10 @@ class Thumbnails {
data class Thumbnail(val url : String?, val quality : Int = 0) { data class Thumbnail(val url : String?, val quality : Int = 0) {
companion object { companion object {
fun fromV8(value: V8ValueObject): Thumbnail { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
return Thumbnail( return Thumbnail(
value.getString("url"), value.getOrDefault<String>(config,"url", "Thumbnail", null),
value.getInteger("quality")); value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
} }
} }
}; };
@@ -37,6 +37,10 @@ class SerializedChannel(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
fun isSameUrl(url: String): Boolean {
return this.url == url || urlAlternatives.contains(url);
}
companion object { companion object {
fun fromChannel(channel: IPlatformChannel): SerializedChannel { fun fromChannel(channel: IPlatformChannel): SerializedChannel {
return SerializedChannel( return SerializedChannel(
@@ -14,7 +14,8 @@ enum class ChapterType(val value: Int) {
NORMAL(0), NORMAL(0),
SKIPPABLE(5), SKIPPABLE(5),
SKIP(6); SKIP(6),
SKIPONCE(7);
@@ -19,8 +19,9 @@ class PolycentricPlatformComment : IPlatformComment {
val eventPointer: Pointer; val eventPointer: Pointer;
val reference: Reference; val reference: Reference;
val parentReference: Reference?;
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) { constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
this.contextUrl = contextUrl; this.contextUrl = contextUrl;
this.author = author; this.author = author;
this.message = msg; this.message = msg;
@@ -29,6 +30,7 @@ class PolycentricPlatformComment : IPlatformComment {
this.replyCount = replyCount; this.replyCount = replyCount;
this.eventPointer = eventPointer; this.eventPointer = eventPointer;
this.reference = eventPointer.toReference(); this.reference = eventPointer.toReference();
this.parentReference = parentReference;
} }
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> { override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
@@ -36,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
} }
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment { fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount); return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
} }
companion object { companion object {
private const val TAG = "PolycentricPlatformComment"
val MAX_COMMENT_SIZE = 2000 val MAX_COMMENT_SIZE = 2000
} }
} }
@@ -1,31 +1,23 @@
package com.futo.platformplayer.api.media.models.live package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.ratings.RatingScaler
import com.futo.platformplayer.api.media.models.ratings.RatingType
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault
interface IPlatformLiveEvent { interface IPlatformLiveEvent {
val type : LiveEventType; val type : LiveEventType;
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IPlatformLiveEvent { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
val contextName = "LiveEvent"; val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
val type = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); return when(t) {
return when(type) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj); LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj); LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj);
LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj); LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj);
LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj); LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj);
LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj); LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj);
else -> throw NotImplementedError("Unknown type ${type}"); else -> throw NotImplementedError("Unknown type $t");
} }
} }
} }
@@ -0,0 +1,14 @@
package com.futo.platformplayer.api.media.models.modifier
class AdhocRequestModifier: IRequestModifier {
val _handler: (String, Map<String,String>)->IRequest;
override var allowByteSkip: Boolean = false;
constructor(modifyReq: (String, Map<String,String>)->IRequest) {
_handler = modifyReq;
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
return _handler(url, headers);
}
}
@@ -0,0 +1,7 @@
package com.futo.platformplayer.api.media.models.modifier
interface IModifierOptions {
val applyAuthClient: String?;
val applyCookieClient: String?;
val applyOtherHeaders: Boolean;
}
@@ -0,0 +1,6 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequest {
val url: String?;
val headers: Map<String, String>;
}
@@ -0,0 +1,7 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequestModifier {
var allowByteSkip: Boolean;
fun modifyRequest(url: String, headers: Map<String, String>): IRequest
}
@@ -1,9 +1,6 @@
package com.futo.platformplayer.api.media.models.playlists package com.futo.platformplayer.api.media.models.playlists
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
interface IPlatformPlaylist : IPlatformContent { interface IPlatformPlaylist : IPlatformContent {
val thumbnail: String?; val thumbnail: String?;
@@ -2,10 +2,6 @@ package com.futo.platformplayer.api.media.models.post
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
/** /**
* A detailed video model with data including video/audio sources * A detailed video model with data including video/audio sources
@@ -14,14 +14,13 @@ interface IRating {
companion object { companion object {
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) }; fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IRating { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
val contextName = "Rating"; val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
val type = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); return when(t) {
return when(type) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj); RatingType.LIKES -> RatingLikes.fromV8(config, obj);
RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj); RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj);
RatingType.SCALE -> RatingScaler.fromV8(config, obj); RatingType.SCALE -> RatingScaler.fromV8(config, obj);
else -> throw NotImplementedError("Unknown type ${type}"); else -> throw NotImplementedError("Unknown type $t");
} }
} }
} }
@@ -1,8 +1,6 @@
package com.futo.platformplayer.api.media.models.subtitles package com.futo.platformplayer.api.media.models.subtitles
import android.net.Uri import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
interface ISubtitleSource { interface ISubtitleSource {
val name: String; val name: String;
@@ -1,13 +1,12 @@
package com.futo.platformplayer.api.media.models.video package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.* import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.structures.IPager
/** /**
* A detailed video model with data including video/audio sources * A detailed video model with data including video/audio sources
@@ -8,6 +8,5 @@ import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
class SerializedVideoMuxedSourceDescriptor( class SerializedVideoMuxedSourceDescriptor(
val _videoSources: Array<VideoUrlSource> val _videoSources: Array<VideoUrlSource>
): VideoMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor { ): VideoMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
@kotlinx.serialization.Transient
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray(); override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
}; };
@@ -1,15 +1,16 @@
package com.futo.platformplayer.api.media.models.video package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.* import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
class SerializedVideoNonMuxedSourceDescriptor( class SerializedVideoNonMuxedSourceDescriptor(
val _videoSources: Array<VideoUrlSource>, val _videoSources: Array<VideoUrlSource>,
val _audioSources: Array<AudioUrlSource> val _audioSources: Array<AudioUrlSource>
): VideoUnMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor { ): VideoUnMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
@kotlinx.serialization.Transient
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray(); override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
@kotlinx.serialization.Transient
override val audioSources: Array<IAudioSource> get() = _audioSources.map { it }.toTypedArray(); override val audioSources: Array<IAudioSource> get() = _audioSources.map { it }.toTypedArray();
}; };
@@ -1,14 +1,14 @@
package com.futo.platformplayer.api.media.platforms.js package com.futo.platformplayer.api.media.platforms.js
import android.content.Context import android.content.Context
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import java.util.* import com.futo.platformplayer.states.StateDeveloper
import java.util.UUID
class DevJSClient : JSClient { class DevJSClient : JSClient {
override val id: String override val id: String
@@ -20,14 +20,14 @@ class DevJSClient : JSClient {
val devID: String; val devID: String;
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV")), null, script) { constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null, settings: HashMap<String, String?>? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV"), settings), null, script) {
_devScript = script; _devScript = script;
_auth = auth; _auth = auth;
_captcha = captcha; _captcha = captcha;
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5); this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
onCaptchaException.subscribe { client, captcha -> onCaptchaException.subscribe { client, c ->
StateApp.instance.handleCaptchaException(client, captcha); StateApp.instance.handleCaptchaException(client, c);
} }
} }
//TODO: Misisng auth/captcha pass on purpose? //TODO: Misisng auth/captcha pass on purpose?
@@ -37,8 +37,8 @@ class DevJSClient : JSClient {
_captcha = captcha; _captcha = captcha;
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5); this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
onCaptchaException.subscribe { client, captcha -> onCaptchaException.subscribe { client, c ->
StateApp.instance.handleCaptchaException(client, captcha); StateApp.instance.handleCaptchaException(client, c);
} }
} }
@@ -49,7 +49,7 @@ class DevJSClient : JSClient {
_auth = auth; _auth = auth;
} }
fun recreate(context: Context): DevJSClient { fun recreate(context: Context): DevJSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID); return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
} }
override fun getCopy(): JSClient { override fun getCopy(): JSClient {
@@ -4,12 +4,10 @@ import android.content.Context
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueNull
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
@@ -23,43 +21,61 @@ import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.internal.* import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
import com.futo.platformplayer.api.media.platforms.js.models.* import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
import com.futo.platformplayer.api.media.platforms.js.models.JSCommentPager
import com.futo.platformplayer.api.media.platforms.js.models.JSContentPager
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveChatWindowDescriptor
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.PluginEngineException import com.futo.platformplayer.engine.exceptions.PluginEngineException
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptValidationException import com.futo.platformplayer.engine.exceptions.ScriptValidationException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import kotlin.reflect.full.findAnnotations import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction import kotlin.reflect.jvm.kotlinFunction
import kotlin.streams.asSequence
open class JSClient : IPlatformClient { open class JSClient : IPlatformClient {
val config: SourcePluginConfig; val config: SourcePluginConfig;
protected val _context: Context; protected val _context: Context;
private val _plugin: V8Plugin; private val _plugin: V8Plugin;
private val plugin: V8Plugin get() = _plugin ?: throw IllegalStateException("Client not enabled"); private val plugin: V8Plugin get() = _plugin
var descriptor: SourcePluginDescriptor var descriptor: SourcePluginDescriptor
private set; private set;
private val _client: JSHttpClient; private val _httpClient: JSHttpClient;
private val _clientAuth: JSHttpClient?; private val _httpClientAuth: JSHttpClient?;
private var _searchCapabilities: ResultCapabilities? = null; private var _searchCapabilities: ResultCapabilities? = null;
private var _searchChannelContentsCapabilities: ResultCapabilities? = null; private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
private var _channelCapabilities: ResultCapabilities? = null; private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null;
protected val _script: String; protected val _script: String;
@@ -78,7 +94,11 @@ open class JSClient : IPlatformClient {
private val _busyLock = Object(); private val _busyLock = Object();
private var _busyCounter = 0; private var _busyCounter = 0;
private var _busyAction = "";
val isBusy: Boolean get() = _busyCounter > 0; val isBusy: Boolean get() = _busyCounter > 0;
val isBusyAction: String get() {
return _busyAction;
}
val settings: HashMap<String, String?> get() = descriptor.settings; val settings: HashMap<String, String?> get() = descriptor.settings;
@@ -119,9 +139,9 @@ open class JSClient : IPlatformClient {
_captcha = descriptor.getCaptchaData(); _captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray(); flags = descriptor.flags.toTypedArray();
_client = JSHttpClient(this, null, _captcha); _httpClient = JSHttpClient(this, null, _captcha);
_clientAuth = JSHttpClient(this, _auth, _captcha); _httpClientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth); _plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
_plugin.withDependency(context, "scripts/polyfil.js"); _plugin.withDependency(context, "scripts/polyfil.js");
_plugin.withDependency(context, "scripts/source.js"); _plugin.withDependency(context, "scripts/source.js");
@@ -137,6 +157,8 @@ open class JSClient : IPlatformClient {
if(it is ScriptCaptchaRequiredException) if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it); onCaptchaException.emit(this, it);
}; };
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
} }
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) { constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
this._context = context; this._context = context;
@@ -148,9 +170,9 @@ open class JSClient : IPlatformClient {
_captcha = descriptor.getCaptchaData(); _captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray(); flags = descriptor.flags.toTypedArray();
_client = JSHttpClient(this, null, _captcha); _httpClient = JSHttpClient(this, null, _captcha);
_clientAuth = JSHttpClient(this, _auth, _captcha); _httpClientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth); _plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
_plugin.withDependency(context, "scripts/polyfil.js"); _plugin.withDependency(context, "scripts/polyfil.js");
_plugin.withDependency(context, "scripts/source.js"); _plugin.withDependency(context, "scripts/source.js");
_plugin.withScript(script); _plugin.withScript(script);
@@ -160,6 +182,8 @@ open class JSClient : IPlatformClient {
if(it is ScriptCaptchaRequiredException) if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it); onCaptchaException.emit(this, it);
}; };
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
} }
open fun getCopy(): JSClient { open fun getCopy(): JSClient {
@@ -169,6 +193,13 @@ open class JSClient : IPlatformClient {
fun getUnderlyingPlugin(): V8Plugin { fun getUnderlyingPlugin(): V8Plugin {
return _plugin; return _plugin;
} }
fun getHttpClientById(id: String): JSHttpClient? {
if(_httpClient.clientId == id)
return _httpClient;
if(_httpClientAuth?.clientId == id)
return _httpClientAuth;
return plugin.httpClientOthers[id];
}
override fun initialize() { override fun initialize() {
Logger.i(TAG, "Plugin [${config.name}] initializing"); Logger.i(TAG, "Plugin [${config.name}] initializing");
@@ -194,9 +225,11 @@ open class JSClient : IPlatformClient {
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false, hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false, hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false, hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false, hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false, hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false, hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
); );
try { try {
@@ -240,15 +273,15 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform") @JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
override fun getHome(): IPager<IPlatformContent> = isBusyWith { override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSContentPager(config, plugin, return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getHome()")); plugin.executeTyped("source.getHome()"));
} }
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query") @JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
@JSDocsParameter("query", "Query to complete suggestions for") @JSDocsParameter("query", "Query to complete suggestions for")
override fun searchSuggestions(query: String): Array<String> = isBusyWith { override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
ensureEnabled(); ensureEnabled();
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})") return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
.toArray() .toArray()
@@ -278,14 +311,17 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("order", "(optional) Order in which contents should be returned") @JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents") @JSDocsParameter("filters", "(optional) Filters to apply on contents")
@JSDocsParameter("channelId", "(optional) Channel id to search in") @JSDocsParameter("channelId", "(optional) Channel id to search in")
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSContentPager(config, plugin, return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
} }
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos") @JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
override fun getSearchChannelContentsCapabilities(): ResultCapabilities { override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
if(!capabilities.hasGetSearchChannelContentsCapabilities)
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
ensureEnabled(); ensureEnabled();
if (_searchChannelContentsCapabilities != null) if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!; return _searchChannelContentsCapabilities!!;
@@ -299,21 +335,21 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("type", "(optional) Type of contents to get from search ") @JSDocsParameter("type", "(optional) Type of contents to get from search ")
@JSDocsParameter("order", "(optional) Order in which contents should be returned") @JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents") @JSDocsParameter("filters", "(optional) Filters to apply on contents")
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
ensureEnabled(); ensureEnabled();
if(!capabilities.hasSearchChannelContents) if(!capabilities.hasSearchChannelContents)
throw IllegalStateException("This plugin does not support channel search"); throw IllegalStateException("This plugin does not support channel search");
return@isBusyWith JSContentPager(config, plugin, return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
} }
@JSOptional @JSOptional
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform") @JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
@JSDocsParameter("query", "Query that channels should match") @JSDocsParameter("query", "Query that channels should match")
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith { override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSChannelPager(config, plugin, return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})")); plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
} }
@@ -331,7 +367,7 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url") @JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@JSDocsParameter("channelUrl", "A channel url (this platform)") @JSDocsParameter("channelUrl", "A channel url (this platform)")
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith { override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSChannel(config, return@isBusyWith JSChannel(config,
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})")); plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
@@ -358,12 +394,46 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("type", "(optional) Type of contents to get from channel") @JSDocsParameter("type", "(optional) Type of contents to get from channel")
@JSDocsParameter("order", "(optional) Order in which contents should be returned") @JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents") @JSDocsParameter("filters", "(optional) Filters to apply on contents")
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSContentPager(config, plugin, return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
} }
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
override fun getPeekChannelTypes(): List<String> {
if(!capabilities.hasPeekChannelContents)
return listOf();
try {
if (_peekChannelTypes != null) {
return _peekChannelTypes!!;
}
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
_peekChannelTypes = arr.keys.mapNotNull {
val str = arr.get<V8ValueString>(it);
return@mapNotNull str.value;
};
return _peekChannelTypes ?: listOf();
}
catch(ex: Throwable) {
announcePluginUnhandledException("getPeekChannelTypes", ex);
return listOf();
}
}
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
@JSDocsParameter("channelUrl", "A channel url (this platform)")
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
ensureEnabled();
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
return@isBusyWith items.keys.mapNotNull {
val obj = items.get<V8ValueObject>(it);
return@mapNotNull IJSContent.fromV8(this, obj);
};
}
@JSOptional @JSOptional
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim") @JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
@JSDocsParameter("claimType", "Polycentric claimtype id") @JSDocsParameter("claimType", "Polycentric claimtype id")
@@ -424,16 +494,16 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url") @JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@JSDocsParameter("url", "A content url (this platform)") @JSDocsParameter("url", "A content url (this platform)")
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith { override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
ensureEnabled(); ensureEnabled();
return@isBusyWith IJSContentDetails.fromV8(config, return@isBusyWith IJSContentDetails.fromV8(this,
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})")); plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
} }
@JSOptional //getContentChapters = function(url, initialData) @JSOptional //getContentChapters = function(url, initialData)
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details") @JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
@JSDocsParameter("url", "A content url (this platform)") @JSDocsParameter("url", "A content url (this platform)")
override fun getContentChapters(url: String): List<IChapter> = isBusyWith { override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
if(!capabilities.hasGetContentChapters) if(!capabilities.hasGetContentChapters)
return@isBusyWith listOf(); return@isBusyWith listOf();
ensureEnabled(); ensureEnabled();
@@ -444,7 +514,7 @@ open class JSClient : IPlatformClient {
@JSOptional @JSOptional
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url") @JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
@JSDocsParameter("url", "A content url (this platform)") @JSDocsParameter("url", "A content url (this platform)")
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith { override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
if(!capabilities.hasGetPlaybackTracker) if(!capabilities.hasGetPlaybackTracker)
return@isBusyWith null; return@isBusyWith null;
ensureEnabled(); ensureEnabled();
@@ -458,25 +528,25 @@ open class JSClient : IPlatformClient {
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url") @JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
@JSDocsParameter("url", "A content url (this platform)") @JSDocsParameter("url", "A content url (this platform)")
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith { override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
ensureEnabled(); ensureEnabled();
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})"); val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
if (pager !is V8ValueObject) { //TODO: Maybe solve this better if (pager !is V8ValueObject) { //TODO: Maybe solve this better
return@isBusyWith EmptyPager<IPlatformComment>(); return@isBusyWith EmptyPager<IPlatformComment>();
} }
return@isBusyWith JSCommentPager(config, plugin, pager); return@isBusyWith JSCommentPager(config, this, pager);
} }
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment") @JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
@JSDocsParameter("comment", "Comment object that was returned by getComments") @JSDocsParameter("comment", "Comment object that was returned by getComments")
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> { override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
ensureEnabled(); ensureEnabled();
return comment.getReplies(this) ?: JSCommentPager(config, plugin, return comment.getReplies(this) ?: JSCommentPager(config, this,
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})")); plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
} }
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream") @JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream") @JSDocsParameter("url", "Url of live stream")
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith { override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
if(!capabilities.hasGetLiveChatWindow) if(!capabilities.hasGetLiveChatWindow)
return@isBusyWith null; return@isBusyWith null;
ensureEnabled(); ensureEnabled();
@@ -485,11 +555,11 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream") @JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream") @JSDocsParameter("url", "Url of live stream")
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith { override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
if(!capabilities.hasGetLiveEvents) if(!capabilities.hasGetLiveEvents)
return@isBusyWith null; return@isBusyWith null;
ensureEnabled(); ensureEnabled();
return@isBusyWith JSLiveEventPager(config, plugin, return@isBusyWith JSLiveEventPager(config, this,
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})")); plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
} }
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform") @JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
@@ -498,27 +568,34 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("order", "(optional) Order in which contents should be returned") @JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents") @JSDocsParameter("filters", "(optional) Filters to apply on contents")
@JSDocsParameter("channelId", "(optional) Channel id to search in") @JSDocsParameter("channelId", "(optional) Channel id to search in")
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
ensureEnabled(); ensureEnabled();
if(!capabilities.hasSearchPlaylists) if(!capabilities.hasSearchPlaylists)
throw IllegalStateException("This plugin does not support playlist search"); throw IllegalStateException("This plugin does not support playlist search");
return@isBusyWith JSContentPager(config, plugin, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
} }
@JSOptional @JSOptional
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform") @JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist") @JSDocsParameter("url", "Url of playlist")
override fun isPlaylistUrl(url: String): Boolean { override fun isPlaylistUrl(url: String): Boolean {
ensureEnabled();
if (!capabilities.hasGetPlaylist) if (!capabilities.hasGetPlaylist)
return false; return false;
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
try {
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex);
return false;
}
} }
@JSOptional @JSOptional
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user") @JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
@JSDocsParameter("url", "Url of playlist") @JSDocsParameter("url", "Url of playlist")
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith { override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSPlaylistDetails(plugin, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})")); return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
} }
@JSOptional @JSOptional
@@ -613,19 +690,24 @@ open class JSClient : IPlatformClient {
} }
private fun <T> isBusyWith(handle: ()->T): T { private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
try { try {
synchronized(_busyLock) { synchronized(_busyLock) {
_busyCounter++; _busyCounter++;
} }
_busyAction = actionName;
return handle(); return handle();
} }
finally { finally {
_busyAction = "";
synchronized(_busyLock) { synchronized(_busyLock) {
_busyCounter--; _busyCounter--;
} }
} }
} }
private fun <T> isBusyWith(handle: ()->T): T {
return isBusyWith("Unknown", handle);
}
private fun announcePluginUnhandledException(method: String, ex: Throwable) { private fun announcePluginUnhandledException(method: String, ex: Throwable) {
if(ex is PluginEngineException) if(ex is PluginEngineException)
@@ -642,10 +724,43 @@ open class JSClient : IPlatformClient {
companion object { companion object {
val TAG = "JSClient"; val TAG = "JSClient";
private val _lock = Object();
private var _docs: Map<String, String>? = null;
fun getMethodDocs(names: List<String>): Map<String, String>? {
synchronized(_lock) {
if(_docs == null) {
val client = ManagedHttpClient();
val docs = names
.map { stringWithoutBrackets(it) }
.distinct()
.parallelStream()
.map {
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
val resp = client.head(url);
if(resp.isOk)
return@map Pair(it, url);
else
return@map null;
}.asSequence()
.filterNotNull()
.toMap();
_docs = docs;
}
return _docs;
}
}
fun getMethodDocUrls(): Map<String, String>? {
if(_docs != null)
return _docs;
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
return getMethodDocs(methods.map { it.name });
}
fun getJSDocs(): List<JSCallDocs> { fun getJSDocs(): List<JSCallDocs> {
val docs = mutableListOf<JSCallDocs>(); val docs = mutableListOf<JSCallDocs>();
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null } val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) { for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
val doc = method.getAnnotation(JSDocs::class.java); val doc = method.getAnnotation(JSDocs::class.java);
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>(); val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
@@ -658,5 +773,12 @@ open class JSClient : IPlatformClient {
} }
return docs; return docs;
} }
private fun stringWithoutBrackets(name: String): String {
val index = name.indexOf('(');
if(index >= 0)
return name.substring(0, index);
return name;
}
} }
} }
@@ -11,4 +11,5 @@ class SourcePluginAuthConfig(
val userAgent: String? = null, val userAgent: String? = null,
val loginButton: String? = null, val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<String>>? = null, val domainHeadersToFind: Map<String, List<String>>? = null,
val loginWarning: String? = null
) { } ) { }
@@ -5,9 +5,8 @@ import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.decodeFromString
import java.net.URL import java.net.URL
import java.util.* import java.util.UUID
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
class SourcePluginConfig( class SourcePluginConfig(
@@ -46,7 +45,8 @@ class SourcePluginConfig(
var enableInSearch: Boolean = true, var enableInSearch: Boolean = true,
var enableInHome: Boolean = true, var enableInHome: Boolean = true,
var supportedClaimTypes: List<Int> = listOf(), var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null
) : IV8PluginConfig { ) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl); val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@@ -149,7 +149,6 @@ class SourcePluginConfig(
val warningDialog: String? = null, val warningDialog: String? = null,
val options: List<String>? = null val options: List<String>? = null
) { ) {
@kotlinx.serialization.Transient
val variableOrName: String get() = variable ?: name; val variableOrName: String get() = variable ?: name;
} }
} }
@@ -2,10 +2,13 @@ package com.futo.platformplayer.api.media.platforms.js
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.views.fields.DropdownFieldOptions import com.futo.platformplayer.views.fields.DropdownFieldOptions
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@@ -27,17 +30,19 @@ class SourcePluginDescriptor {
@kotlinx.serialization.Transient @kotlinx.serialization.Transient
val onCaptchaChanged = Event0(); val onCaptchaChanged = Event0();
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null) { constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, settings: HashMap<String, String?>? = null) {
this.config = config; this.config = config;
this.authEncrypted = authEncrypted; this.authEncrypted = authEncrypted;
this.captchaEncrypted = captchaEncrypted; this.captchaEncrypted = captchaEncrypted;
this.flags = listOf(); this.flags = listOf();
this.settings = settings ?: hashMapOf();
} }
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) { constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>, settings: HashMap<String, String?>? = null) {
this.config = config; this.config = config;
this.authEncrypted = authEncrypted; this.authEncrypted = authEncrypted;
this.captchaEncrypted = captchaEncrypted; this.captchaEncrypted = captchaEncrypted;
this.flags = flags; this.flags = flags;
this.settings = settings ?: hashMapOf();
} }
fun getSettingsWithDefaults(): HashMap<String, String?> { fun getSettingsWithDefaults(): HashMap<String, String?> {
@@ -54,7 +59,16 @@ class SourcePluginDescriptor {
onCaptchaChanged.emit(); onCaptchaChanged.emit();
} }
fun getCaptchaData(): SourceCaptchaData? { fun getCaptchaData(): SourceCaptchaData? {
return SourceCaptchaData.fromEncrypted(captchaEncrypted); try {
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
}
catch(ex: Throwable) {
Logger.e("SourcePluginDescriptor", "Captcha decode failed, disabling auth.", ex);
StateAnnouncement.instance.registerAnnouncement("CAP_BROKEN_" + config.id,
"Captcha corrupted for plugin [${config.name}]",
"Something went wrong in the stored captcha, you'll have to login again", AnnouncementType.SESSION);
return null;
}
} }
fun updateAuth(str: SourceAuth?) { fun updateAuth(str: SourceAuth?) {
@@ -62,12 +76,24 @@ class SourcePluginDescriptor {
onAuthChanged.emit(); onAuthChanged.emit();
} }
fun getAuth(): SourceAuth? { fun getAuth(): SourceAuth? {
return SourceAuth.fromEncrypted(authEncrypted); try {
return SourceAuth.fromEncrypted(authEncrypted);
}
catch(ex: Throwable) {
Logger.e("SourcePluginDescriptor", "Authentication decode failed, disabling auth.", ex);
StateAnnouncement.instance.registerAnnouncement("AUTH_BROKEN_" + config.id,
"Authentication corrupted for plugin [${config.name}]",
"Something went wrong in the stored authentication, you'll have to login again", AnnouncementType.SESSION);
return null;
}
} }
@Serializable @Serializable
class AppPluginSettings { class AppPluginSettings {
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
var checkForUpdates: Boolean = true;
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2) @FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
var tabEnabled = TabEnabled(); var tabEnabled = TabEnabled();
@Serializable @Serializable
@@ -105,11 +131,16 @@ class SourcePluginDescriptor {
} }
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
var allowDeveloperSubmit: Boolean = false;
fun loadDefaults(config: SourcePluginConfig) { fun loadDefaults(config: SourcePluginConfig) {
if(tabEnabled.enableHome == null) if(tabEnabled.enableHome == null)
tabEnabled.enableHome = config.enableInHome ?: true; tabEnabled.enableHome = config.enableInHome
if(tabEnabled.enableSearch == null) if(tabEnabled.enableSearch == null)
tabEnabled.enableSearch = config.enableInSearch ?: true; tabEnabled.enableSearch = config.enableInSearch
} }
} }
@@ -14,6 +14,6 @@ annotation class JSOptional()
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0) annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false); data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false, val docsUrl: String? = null);
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class JSParameterDocs(val name: String, val description: String); data class JSParameterDocs(val name: String, val description: String);
@@ -1,14 +1,16 @@
package com.futo.platformplayer.api.media.platforms.js.internal package com.futo.platformplayer.api.media.platforms.js.internal
import android.net.Uri import android.net.Uri
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.matchesDomain
import java.util.UUID
class JSHttpClient : ManagedHttpClient { class JSHttpClient : ManagedHttpClient {
private val _jsClient: JSClient?; private val _jsClient: JSClient?;
@@ -16,12 +18,15 @@ class JSHttpClient : ManagedHttpClient {
private val _auth: SourceAuth?; private val _auth: SourceAuth?;
private val _captcha: SourceCaptchaData?; private val _captcha: SourceCaptchaData?;
val clientId = UUID.randomUUID().toString();
var doUpdateCookies: Boolean = true; var doUpdateCookies: Boolean = true;
var doApplyCookies: Boolean = true; var doApplyCookies: Boolean = true;
var doAllowNewCookies: Boolean = true; var doAllowNewCookies: Boolean = true;
val isLoggedIn: Boolean get() = _auth != null; val isLoggedIn: Boolean get() = _auth != null;
private var _currentCookieMap: HashMap<String, HashMap<String, String>>; private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() { constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
_jsClient = jsClient; _jsClient = jsClient;
@@ -30,6 +35,7 @@ class JSHttpClient : ManagedHttpClient {
_captcha = captcha; _captcha = captcha;
_currentCookieMap = hashMapOf(); _currentCookieMap = hashMapOf();
_otherCookieMap = hashMapOf();
if(!auth?.cookieMap.isNullOrEmpty()) { if(!auth?.cookieMap.isNullOrEmpty()) {
for(domainCookies in auth!!.cookieMap!!) for(domainCookies in auth!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value)); _currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
@@ -47,13 +53,49 @@ class JSHttpClient : ManagedHttpClient {
override fun clone(): ManagedHttpClient { override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth); val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = if(_currentCookieMap != null) newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
else
hashMapOf();
return newClient; return newClient;
} }
//TODO: Use this in beforeRequest to remove dup code
fun applyHeaders(url: Uri, headers: MutableMap<String, String>, applyAuth: Boolean = false, applyOtherCookies: Boolean = false) {
val domain = url.host!!.lowercase();
val auth = _auth;
if (applyAuth && auth != null) {
//TODO: Possibly add doApplyHeaders
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
headers.put(header.key, header.value);
}
if(doApplyCookies && (applyAuth || applyOtherCookies)) {
val cookiesToApply = hashMapOf<String, String>();
if(applyOtherCookies)
synchronized(_otherCookieMap) {
for(cookie in _otherCookieMap
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
}
if(applyAuth)
synchronized(_currentCookieMap) {
for(cookie in _currentCookieMap
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
};
if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
val existingCookies = headers["Cookie"];
if(!existingCookies.isNullOrEmpty())
headers.put("Cookie", existingCookies.trim(';') + "; " + cookieString);
else
headers.put("Cookie", cookieString);
}
}
}
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request { override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
val domain = request.url.host.lowercase(); val domain = request.url.host.lowercase();
val auth = _auth; val auth = _auth;
@@ -69,10 +111,10 @@ class JSHttpClient : ManagedHttpClient {
} }
if(doApplyCookies) { if(doApplyCookies) {
if (!_currentCookieMap.isNullOrEmpty()) { if (_currentCookieMap.isNotEmpty()) {
val cookiesToApply = hashMapOf<String, String>(); val cookiesToApply = hashMapOf<String, String>();
synchronized(_currentCookieMap!!) { synchronized(_currentCookieMap) {
for(cookie in _currentCookieMap!! for(cookie in _currentCookieMap
.filter { domain.matchesDomain(it.key) } .filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() }) .flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second; cookiesToApply[cookie.first] = cookie.second;
@@ -92,11 +134,11 @@ class JSHttpClient : ManagedHttpClient {
} }
if(_jsClient != null) if(_jsClient != null)
_jsClient?.validateUrlOrThrow(request.url.toString()); _jsClient.validateUrlOrThrow(request.url.toString());
else if (_jsConfig != null && !_jsConfig.isUrlAllowed(request.url.toString())) else if (_jsConfig != null && !_jsConfig.isUrlAllowed(request.url.toString()))
throw ScriptImplementationException(_jsConfig, "Attempted to access non-whitelisted url: ${request.url.toString()}\nAdd it to your config"); throw ScriptImplementationException(_jsConfig, "Attempted to access non-whitelisted url: ${request.url.toString()}\nAdd it to your config");
return newBuilder?.let { it.build() } ?: request; return newBuilder?.build() ?: request;
} }
override fun afterRequest(resp: okhttp3.Response): okhttp3.Response { override fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
@@ -106,85 +148,65 @@ class JSHttpClient : ManagedHttpClient {
val defaultCookieDomain = val defaultCookieDomain =
"." + domainParts.drop(domainParts.size - 2).joinToString("."); "." + domainParts.drop(domainParts.size - 2).joinToString(".");
for (header in resp.headers) { for (header in resp.headers) {
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") { if(header.first.lowercase() == "set-cookie") {
//val newCookies = cookieStringToMap(header.second.split("; ")); var domainToUse = domain;
val cookie = cookieStringToPair(header.second); val cookie = cookieStringToPair(header.second);
//for (cookie in newCookies) { var cookieValue = cookie.second;
var cookieValue = cookie.second;
var domainToUse = domain;
if (!cookie.first.isNullOrEmpty() && !cookie.second.isNullOrEmpty()) { if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
val cookieParts = cookie.second.split(";"); val cookieParts = cookie.second.split(";");
if (cookieParts.size == 0) if (cookieParts.size == 0)
continue; continue;
cookieValue = cookieParts[0].trim(); cookieValue = cookieParts[0].trim();
val cookieVariables = cookieParts.drop(1).map { val cookieVariables = cookieParts.drop(1).map {
val splitIndex = it.indexOf("="); val splitIndex = it.indexOf("=");
if (splitIndex < 0) if (splitIndex < 0)
return@map Pair(it.trim().lowercase(), ""); return@map Pair(it.trim().lowercase(), "");
return@map Pair<String, String>( return@map Pair<String, String>(
it.substring(0, splitIndex).lowercase().trim(), it.substring(0, splitIndex).lowercase().trim(),
it.substring(splitIndex + 1).trim() it.substring(splitIndex + 1).trim()
); );
}.toMap(); }.toMap();
domainToUse = if (cookieVariables.containsKey("domain")) domainToUse = if (cookieVariables.containsKey("domain"))
cookieVariables["domain"]!!.lowercase(); cookieVariables["domain"]!!.lowercase();
else defaultCookieDomain; else defaultCookieDomain;
} //TODO: Make sure this has no negative effect besides apply cookies to root domain
if(!domainToUse.startsWith("."))
domainToUse = ".${domainToUse}";
}
val cookieMap = if (_currentCookieMap!!.containsKey(domainToUse)) if ((_auth != null || _currentCookieMap.isNotEmpty())) {
_currentCookieMap!![domainToUse]!!; val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
_currentCookieMap[domainToUse]!!;
else { else {
val newMap = hashMapOf<String, String>(); val newMap = hashMapOf<String, String>();
_currentCookieMap!!.put(domainToUse, newMap) _currentCookieMap[domainToUse] = newMap
newMap; newMap;
} }
if(cookieMap.containsKey(cookie.first) || doAllowNewCookies) if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
cookieMap.put(cookie.first, cookieValue); cookieMap[cookie.first] = cookieValue;
//} }
else {
val cookieMap = if (_otherCookieMap.containsKey(domainToUse))
_otherCookieMap[domainToUse]!!;
else {
val newMap = hashMapOf<String, String>();
_otherCookieMap[domainToUse] = newMap
newMap;
}
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
cookieMap[cookie.first] = cookieValue;
}
} }
} }
} }
return resp; return resp;
} }
private fun cookieStringToMap(parts: List<String>): Map<String, String> {
val map = hashMapOf<String, String>();
for(cookie in parts) {
val pair = cookieStringToPair(cookie)
map.put(pair.first, pair.second);
}
return map;
}
private fun cookieStringToPair(cookie: String): Pair<String, String> { private fun cookieStringToPair(cookie: String): Pair<String, String> {
val cookieKey = cookie.substring(0, cookie.indexOf("=")); val cookieKey = cookie.substring(0, cookie.indexOf("="));
val cookieVal = cookie.substring(cookie.indexOf("=") + 1); val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
return Pair(cookieKey.trim(), cookieVal.trim()); return Pair(cookieKey.trim(), cookieVal.trim());
} }
//Prints out code for test reproduction..
fun printTestCode(url: String, body: ByteArray?, headers: Map<String, String>, cookieString: String, allHeaders: Map<String, String>? = null) {
var code = "Code: \n";
code += "\nurl = \"${url}\";";
if(body != null)
code += "\nbody = \"${String(body).replace("\"", "\\\"")}\";";
if(headers != null)
for(header in headers) {
code += "\nclient.Headers.Add(\"${header.key}\", \"${header.value}\");";
}
if(cookieString != null)
code += "\nclient.Headers.Add(\"Cookie\", \"${cookieString}\");";
if(allHeaders != null) {
code += "\n//OTHER HEADERS:"
for (header in allHeaders) {
code += "\nclient.Headers.Add(\"${header.key}\", \"${header.value}\");";
}
}
Logger.i("Testing", code);
}
} }
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -10,13 +11,14 @@ import com.futo.platformplayer.getOrThrow
interface IJSContent: IPlatformContent { interface IJSContent: IPlatformContent {
companion object { companion object {
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContent { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
val config = plugin.config;
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem"); val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null); val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
//TODO: Temporary workaround for intercepting details in lists //TODO: Temporary workaround for intercepting details in lists
if(pluginType != null && pluginType.endsWith("Details")) if(pluginType != null && pluginType.endsWith("Details"))
return IJSContentDetails.fromV8(config, obj); return IJSContentDetails.fromV8(plugin, obj);
return when(ContentType.fromInt(type)) { return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideo(config, obj); ContentType.MEDIA -> JSVideo(config, obj);
@@ -4,17 +4,18 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
interface IJSContentDetails: IPlatformContent { interface IJSContentDetails: IPlatformContent {
companion object { companion object {
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContentDetails { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
val type: Int = obj.getOrThrow(config, "contentType", "ContentDetails"); val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
return when(ContentType.fromInt(type)) { return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(config, obj); ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(config, obj); ContentType.POST -> JSPostDetails(plugin.config, obj);
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }
@@ -2,13 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> { class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {} constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): PlatformAuthorLink { override fun convertResult(obj: V8ValueObject): PlatformAuthorLink {
return PlatformAuthorLink.fromV8(config, obj); return PlatformAuthorLink.fromV8(config, obj);
@@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
@@ -60,6 +61,7 @@ class JSComment : IPlatformComment {
return null; return null;
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>()); val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
return JSCommentPager(_config!!, _plugin!!, obj); val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return JSCommentPager(_config!!, plugin, obj);
} }
} }
@@ -2,15 +2,16 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
class JSCommentPager : JSPager<IPlatformComment>, IPager<IPlatformComment> { class JSCommentPager : JSPager<IPlatformComment>, IPager<IPlatformComment> {
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) { } constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) { }
override fun convertResult(obj: V8ValueObject): IPlatformComment { override fun convertResult(obj: V8ValueObject): IPlatformComment {
return JSComment(config, plugin, obj); return JSComment(config, plugin.getUnderlyingPlugin(), obj);
} }
} }
@@ -3,15 +3,16 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
class JSContentPager : JSPager<IPlatformContent>, IPluginSourced { class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
override val sourceConfig: SourcePluginConfig get() = config; override val sourceConfig: SourcePluginConfig get() = config;
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {} constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformContent { override fun convertResult(obj: V8ValueObject): IPlatformContent {
return IJSContent.fromV8(config, obj); return IJSContent.fromV8(plugin, obj);
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPlatformLiveEventPager import com.futo.platformplayer.api.media.structures.IPlatformLiveEventPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
@@ -10,7 +11,7 @@ import com.futo.platformplayer.getOrThrow
class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager { class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
override var nextRequest: Int; override var nextRequest: Int;
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) { constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }
@@ -4,6 +4,7 @@ import android.os.Looper
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
@@ -12,7 +13,7 @@ import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
abstract class JSPager<T> : IPager<T> { abstract class JSPager<T> : IPager<T> {
protected val plugin: V8Plugin; protected val plugin: JSClient;
protected val config: SourcePluginConfig; protected val config: SourcePluginConfig;
protected var pager: V8ValueObject; protected var pager: V8ValueObject;
@@ -21,9 +22,9 @@ abstract class JSPager<T> : IPager<T> {
private var _hasMorePages: Boolean = false; private var _hasMorePages: Boolean = false;
//private var _morePagesWasFalse: Boolean = false; //private var _morePagesWasFalse: Boolean = false;
val isAvailable get() = plugin._runtime?.let { !it.isClosed && !it.isDead } ?: false; val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) { constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) {
this.plugin = plugin; this.plugin = plugin;
this.pager = pager; this.pager = pager;
this.config = config; this.config = config;
@@ -43,7 +44,7 @@ abstract class JSPager<T> : IPager<T> {
override fun nextPage() { override fun nextPage() {
warnIfMainThread("JSPager.nextPage"); warnIfMainThread("JSPager.nextPage");
pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invoke("nextPage", arrayOf<Any>()); pager.invoke("nextPage", arrayOf<Any>());
}; };
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
@@ -13,7 +14,7 @@ import com.futo.platformplayer.models.Playlist
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails { class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
override val contents: IPager<IPlatformVideo>; override val contents: IPager<IPlatformVideo>;
constructor(plugin: V8Plugin, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) { constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails")); contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
} }
@@ -2,13 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
class JSPlaylistPager : JSPager<IPlatformPlaylist>, IPager<IPlatformPlaylist> { class JSPlaylistPager : JSPager<IPlatformPlaylist>, IPager<IPlatformPlaylist> {
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {} constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformPlaylist { override fun convertResult(obj: V8ValueObject): IPlatformPlaylist {
return JSPlaylist(config, obj); return JSPlaylist(config, obj);
@@ -54,6 +54,6 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
private fun getCommentsJS(client: JSClient): JSCommentPager { private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>()); val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager); return JSCommentPager(_pluginConfig, client, commentPager);
} }
} }
@@ -1,18 +1,99 @@
package com.futo.platformplayer.api.media.platforms.js.models package com.futo.platformplayer.api.media.platforms.js.models
import android.net.Uri
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.IModifierOptions
import com.futo.platformplayer.api.media.models.modifier.IRequest
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.logging.Logger
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
class JSRequest : JSRequestModifier.IRequest { class JSRequest : IRequest {
override val url: String; private val _v8Url: String?;
override val headers: Map<String, String>; private val _v8Headers: Map<String, String>?;
private val _v8Options: Options?;
constructor(config: IV8PluginConfig, obj: V8ValueObject) { override var url: String? = null;
override lateinit var headers: Map<String, String>;
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) {
_v8Url = url;
_v8Headers = headers;
_v8Options = options;
initialize(plugin, originalUrl, originalHeaders);
}
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) {
val contextName = "ModifyRequestResponse"; val contextName = "ModifyRequestResponse";
url = obj.getOrThrow(config, "url", contextName); val config = plugin.config;
headers = obj.getOrThrow(config, "headers", contextName); _v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
Options(config, it, applyOtherHeadersByDefault);
} ?: Options(null, null, applyOtherHeadersByDefault);
initialize(plugin, originalUrl, originalHeaders);
}
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
val config = plugin.config;
url = _v8Url ?: originalUrl;
if(_v8Options?.applyOtherHeaders ?: false) {
val headersToSet = _v8Headers?.toMutableMap() ?: mutableMapOf();
if (originalHeaders != null)
for (head in originalHeaders)
if (!headersToSet.containsKey(head.key))
headersToSet[head.key] = head.value;
headers = headersToSet;
}
else
headers = _v8Headers ?: originalHeaders ?: mapOf();
if(_v8Options != null) {
if(_v8Options.applyCookieClient != null && url != null) {
val client = plugin.getHttpClientById(_v8Options.applyCookieClient);
if(client != null) {
val toModifyHeaders = headers.toMutableMap();
client.applyHeaders(Uri.parse(url), toModifyHeaders, false, true);
headers = toModifyHeaders;
}
}
if(_v8Options.applyAuthClient != null && url != null) {
val client = plugin.getHttpClientById(_v8Options.applyAuthClient);
if(client != null) {
val toModifyHeaders = headers.toMutableMap();
client.applyHeaders(Uri.parse(url), toModifyHeaders, true, false);
headers = toModifyHeaders;
}
}
}
}
fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest {
return JSRequest(plugin, _v8Url, _v8Headers, _v8Options, originalUrl, originalHeaders);
}
@kotlinx.serialization.Serializable
class Options: IModifierOptions {
override val applyAuthClient: String?;
override val applyCookieClient: String?;
override val applyOtherHeaders: Boolean;
constructor(config: IV8PluginConfig, obj: V8ValueObject, applyOtherHeadersByDefault: Boolean = false) {
applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null);
applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null);
applyOtherHeaders = obj.getOrDefault(config, "applyOtherHeaders", "JSRequestModifier.options.applyOtherHeaders", applyOtherHeadersByDefault) ?: applyOtherHeadersByDefault;
}
constructor(applyAuthClient: String? = null, applyCookieClient: String? = null, applyOtherHeaders: Boolean = false) {
this.applyAuthClient = applyAuthClient;
this.applyCookieClient = applyCookieClient;
this.applyOtherHeaders = applyOtherHeaders;
}
}
companion object {
} }
} }
@@ -1,19 +1,28 @@
package com.futo.platformplayer.api.media.platforms.js.models package com.futo.platformplayer.api.media.platforms.js.models
import android.net.Uri
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.IRequest
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
class JSRequestModifier { class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient;
private val _config: IV8PluginConfig; private val _config: IV8PluginConfig;
private var _modifier: V8ValueObject; private var _modifier: V8ValueObject;
val allowByteSkip: Boolean; override var allowByteSkip: Boolean;
constructor(config: IV8PluginConfig, modifier: V8ValueObject) { constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin;
this._modifier = modifier; this._modifier = modifier;
this._config = config; this._config = plugin.config;
val config = plugin.config;
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true; allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
@@ -21,22 +30,20 @@ class JSRequestModifier {
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null); throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
} }
fun modifyRequest(url: String, headers: Map<String, String>): IRequest { override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
if (_modifier.isClosed) { if (_modifier.isClosed) {
return Request(url, headers); return Request(url, headers);
} }
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers); _modifier.invoke("modifyRequest", url, headers);
}; } as V8ValueObject;
return JSRequest(_config, result as V8ValueObject); val req = JSRequest(_plugin, result, url, headers);
result.close();
return req;
} }
interface IRequest {
val url: String;
val headers: Map<String, String>;
}
data class Request(override val url: String, override val headers: Map<String, String>) : IRequest; data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
} }
@@ -44,13 +44,14 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
override val subtitles: List<ISubtitleSource>; override val subtitles: List<ISubtitleSource>;
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
val contextName = "VideoDetails"; val contextName = "VideoDetails";
val config = plugin.config;
description = _content.getOrThrow(config, "description", contextName); description = _content.getOrThrow(config, "description", contextName);
video = JSVideoSourceDescriptor.fromV8(config, _content.getOrThrow(config, "video", contextName)); video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
dash = JSSource.fromV8DashNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName)); dash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
hls = JSSource.fromV8HLSNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName)); hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
live = JSSource.fromV8VideoNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName)); live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0)); rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0));
if(!_content.has("subtitles")) if(!_content.has("subtitles"))
@@ -105,6 +106,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return null; return null;
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager); return JSCommentPager(_pluginConfig, client, commentPager);
} }
} }
@@ -2,12 +2,13 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
class JSVideoPager : JSPager<IPlatformVideo> { class JSVideoPager : JSPager<IPlatformVideo> {
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {} constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformVideo { override fun convertResult(obj: V8ValueObject): IPlatformVideo {
return JSVideo(config, obj); return JSVideo(config, obj);
@@ -2,7 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -19,8 +21,9 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
override var priority: Boolean = false; override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_AUDIOURL, config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
val contextName = "AudioUrlSource"; val contextName = "AudioUrlSource";
val config = plugin.config;
bitrate = _obj.getOrThrow(config, "bitrate", contextName); bitrate = _obj.getOrThrow(config, "bitrate", contextName);
container = _obj.getOrThrow(config, "container", contextName); container = _obj.getOrThrow(config, "container", contextName);
@@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource { class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
@@ -22,8 +24,9 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
&& indexEnd != null) && indexEnd != null)
StreamMetaData(initStart, initEnd, indexStart, indexEnd) else null; StreamMetaData(initStart, initEnd, indexStart, indexEnd) else null;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSAudioUrlRangeSource"; val contextName = "JSAudioUrlRangeSource";
val config = plugin.config;
itagId = _obj.getOrDefault(config, "itagId", contextName, null); itagId = _obj.getOrDefault(config, "itagId", contextName, null);
initStart = _obj.getOrDefault(config, "initStart", contextName, null); initStart = _obj.getOrDefault(config, "initStart", contextName, null);
@@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -19,9 +21,9 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource {
override var priority: Boolean = false; override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_DASH, config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
val contextName = "DashSource"; val contextName = "DashSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName); name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName); url = _obj.getOrThrow(config, "url", contextName);
duration = _obj.getOrThrow(config, "duration", contextName); duration = _obj.getOrThrow(config, "duration", contextName);
@@ -4,7 +4,9 @@ import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
@@ -20,8 +22,9 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
override var priority: Boolean = false; override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_HLS, config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
val contextName = "HLSAudioSource"; val contextName = "HLSAudioSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName); name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName); url = _obj.getOrThrow(config, "url", contextName);
@@ -33,7 +36,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
companion object { companion object {
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) }; fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(config, obj); fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj);
} }
} }
@@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -19,8 +21,9 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource {
override var priority: Boolean = false; override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_HLS, config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
val contextName = "HLSSource"; val contextName = "HLSSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName); name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName); url = _obj.getOrThrow(config, "url", contextName);
@@ -1,34 +1,50 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources package com.futo.platformplayer.api.media.platforms.js.models.sources
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.AdhocRequestModifier
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.upstream.HttpDataSource
abstract class JSSource { abstract class JSSource {
protected val _plugin: JSClient;
protected val _config: IV8PluginConfig; protected val _config: IV8PluginConfig;
protected val _obj: V8ValueObject; protected val _obj: V8ValueObject;
private val _hasRequestModifier: Boolean; val hasRequestModifier: Boolean;
private val _requestModifier: JSRequest?;
val type : String; val type : String;
constructor(type: String, config: IV8PluginConfig, obj: V8ValueObject) { constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
this._config = config; this._plugin = plugin;
this._config = plugin.config;
this._obj = obj; this._obj = obj;
this.type = type; this.type = type;
_hasRequestModifier = obj.has("getRequestModifier"); _requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
JSRequest(plugin, it, null, null, true);
}
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
} }
fun getRequestModifier(): JSRequestModifier? { fun getRequestModifier(): IRequestModifier? {
if (!_hasRequestModifier || _obj.isClosed) { if(_requestModifier != null)
return AdhocRequestModifier { url, headers ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
};
if (!hasRequestModifier || _obj.isClosed) {
return null; return null;
} }
@@ -40,16 +56,7 @@ abstract class JSSource {
return null; return null;
} }
return JSRequestModifier(_config, result) return JSRequestModifier(_plugin, result)
}
fun getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier();
return if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else {
DefaultHttpDataSource.Factory();
}
} }
companion object { companion object {
@@ -60,28 +67,28 @@ abstract class JSSource {
const val TYPE_DASH = "DashSource"; const val TYPE_DASH = "DashSource";
const val TYPE_HLS = "HLSSource"; const val TYPE_HLS = "HLSSource";
fun fromV8VideoNullable(config: IV8PluginConfig, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(config, it as V8ValueObject) }; fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
fun fromV8Video(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSource { fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(config, obj); TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(config, obj); TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
TYPE_HLS -> fromV8HLS(config, obj); TYPE_HLS -> fromV8HLS(plugin, obj);
TYPE_DASH -> fromV8Dash(config, obj); TYPE_DASH -> fromV8Dash(plugin, obj);
else -> throw NotImplementedError("Unknown type ${type}"); else -> throw NotImplementedError("Unknown type ${type}");
} }
} }
fun fromV8DashNullable(config: IV8PluginConfig, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(config, it as V8ValueObject) }; fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
fun fromV8Dash(config: IV8PluginConfig, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(config, obj); fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) }; fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(config, obj); fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
fun fromV8Audio(config: IV8PluginConfig, obj: V8ValueObject) : IAudioSource { fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource {
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(config, obj); TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
TYPE_AUDIOURL -> JSAudioUrlSource(config, obj); TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(config, obj); TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
else -> throw NotImplementedError("Unknown type ${type}"); else -> throw NotImplementedError("Unknown type ${type}");
} }
} }
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -15,15 +16,16 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
override val videoSources: Array<IVideoSource>; override val videoSources: Array<IVideoSource>;
override val audioSources: Array<IAudioSource>; override val audioSources: Array<IAudioSource>;
constructor(config: IV8PluginConfig, obj: V8ValueObject) { constructor(plugin: JSClient, obj: V8ValueObject) {
this._obj = obj; this._obj = obj;
val config = plugin.config;
val contextName = "UnMuxVideoSource" val contextName = "UnMuxVideoSource"
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName); this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray() this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(config, it as V8ValueObject) } .map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.toTypedArray(); .toTypedArray();
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray() this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
.map { JSSource.fromV8Audio(config, it as V8ValueObject) } .map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
.toTypedArray(); .toTypedArray();
} }
} }
@@ -5,21 +5,22 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor { class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
protected val _obj: V8ValueObject; protected val _obj: V8ValueObject;
override val isUnMuxed: Boolean; override val isUnMuxed: Boolean;
override val videoSources: Array<IVideoSource>; override val videoSources: Array<IVideoSource>;
constructor(config: IV8PluginConfig, obj: V8ValueObject) { constructor(plugin: JSClient, obj: V8ValueObject) {
this._obj = obj; this._obj = obj;
val config = plugin.config;
val contextName = "VideoSourceDescriptor"; val contextName = "VideoSourceDescriptor";
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName); this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray() this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(config, it as V8ValueObject) } .map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.toTypedArray(); .toTypedArray();
} }
@@ -28,11 +29,11 @@ class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
const val TYPE_UNMUXED = "UnMuxVideoSourceDescriptor"; const val TYPE_UNMUXED = "UnMuxVideoSourceDescriptor";
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSourceDescriptor { fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor {
val type = obj.getString("plugin_type") val type = obj.getString("plugin_type")
return when(type) { return when(type) {
TYPE_MUXED -> JSVideoSourceDescriptor(config, obj); TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj);
TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(config, obj); TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(plugin, obj);
else -> throw NotImplementedError("Unknown type: ${type}"); else -> throw NotImplementedError("Unknown type: ${type}");
} }
} }
@@ -2,7 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -18,8 +20,9 @@ open class JSVideoUrlSource : IVideoUrlSource, JSSource {
override var priority: Boolean = false; override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject): super(TYPE_VIDEOURL, config, obj) { constructor(plugin: JSClient, obj: V8ValueObject): super(TYPE_VIDEOURL, plugin, obj) {
val contextName = "JSVideoUrlSource"; val contextName = "JSVideoUrlSource";
val config = plugin.config;
width = _obj.getOrThrow(config, "width", contextName); width = _obj.getOrThrow(config, "width", contextName);
height = _obj.getOrThrow(config, "height", contextName); height = _obj.getOrThrow(config, "height", contextName);
@@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource { class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
@@ -21,8 +23,9 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
&& indexEnd != null) && indexEnd != null)
StreamMetaData(initStart, initEnd, indexStart, indexEnd) else null; StreamMetaData(initStart, initEnd, indexStart, indexEnd) else null;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSVideoUrlRangeSource"; val contextName = "JSVideoUrlRangeSource";
val config = plugin.config;
itagId = _obj.getOrDefault(config, "itagId", contextName, null); itagId = _obj.getOrDefault(config, "itagId", contextName, null);
initStart = _obj.getOrDefault(config, "initStart", contextName, null); initStart = _obj.getOrDefault(config, "initStart", contextName, null);
@@ -1,7 +1,5 @@
package com.futo.platformplayer.api.media.structures package com.futo.platformplayer.api.media.structures
import kotlinx.coroutines.CoroutineScope
/** /**
* A Pager interface that implements a suspended manner of nextPage * A Pager interface that implements a suspended manner of nextPage
*/ */
@@ -1,6 +1,5 @@
package com.futo.platformplayer.api.media.structures package com.futo.platformplayer.api.media.structures
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
/** /**
@@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asRe
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
/** /**
@@ -25,6 +26,7 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
private val _pending: MutableList<Deferred<IPager<T>?>>; private val _pending: MutableList<Deferred<IPager<T>?>>;
@OptIn(ExperimentalCoroutinesApi::class)
constructor(pagers: List<IPager<T>>, pendingPagers: List<Deferred<IPager<T>?>>, placeholderPagers: List<IPager<T>>? = null) { constructor(pagers: List<IPager<T>>, pendingPagers: List<Deferred<IPager<T>?>>, placeholderPagers: List<IPager<T>>? = null) {
_pagersReusable = pagers.map { ReusablePager(it) }.toMutableList(); _pagersReusable = pagers.map { ReusablePager(it) }.toMutableList();
_totalPagers = pagers.size + pendingPagers.size; _totalPagers = pagers.size + pendingPagers.size;
@@ -100,7 +102,7 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
} }
private fun getCurrentSubPagers(): List<IPager<T>> { private fun getCurrentSubPagers(): List<IPager<T>> {
val reusableWindows = _pagersReusable.map { it.getWindow() as IPager<T> }; val reusableWindows = _pagersReusable.map { it.getWindow() };
val placeholderWindows = synchronized(_pending) { val placeholderWindows = synchronized(_pending) {
_placeHolderPagersPaired.filter { _pending.contains(it.key) }.values _placeHolderPagersPaired.filter { _pending.contains(it.key) }.values
} }
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.structures
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.assume
import kotlin.streams.asSequence
import kotlin.streams.toList import kotlin.streams.toList
/** /**
@@ -28,6 +30,7 @@ class PlatformContentPager : IPager<IPlatformContent> {
_page++; _page++;
_currentItems = _items.stream() _currentItems = _items.stream()
.skip((_page * _pageSize).toLong()) .skip((_page * _pageSize).toLong())
.asSequence()
.toList() .toList()
.take(_pageSize) .take(_pageSize)
.toList(); .toList();
@@ -41,7 +41,7 @@ class SingleAsyncItemPager<T> {
fun getCurrentItem(scope: CoroutineScope) : Deferred<T?>? { fun getCurrentItem(scope: CoroutineScope) : Deferred<T?>? {
synchronized(_requestedPageItems) { synchronized(_requestedPageItems) {
if (_currentResultPos >= _requestedPageItems.size) { if (_currentResultPos >= _requestedPageItems.size) {
val startPos = fillDeferredUntil(_currentResultPos); fillDeferredUntil(_currentResultPos);
if(!_pager.hasMorePages()) { if(!_pager.hasMorePages()) {
Logger.i("SingleAsyncItemPager", "end of async page reached"); Logger.i("SingleAsyncItemPager", "end of async page reached");
completeRemainder { it?.complete(null) }; completeRemainder { it?.complete(null) };
@@ -49,7 +49,7 @@ class SingleAsyncItemPager<T> {
if(_isRequesting) if(_isRequesting)
return _requestedPageItems[_currentResultPos]; return _requestedPageItems[_currentResultPos];
_isRequesting = true; _isRequesting = true;
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
Logger.i("SingleAsyncItemPager", "Started Pager"); Logger.i("SingleAsyncItemPager", "Started Pager");
val timeForPage = measureTimeMillis { _pager.nextPage() }; val timeForPage = measureTimeMillis { _pager.nextPage() };
@@ -100,7 +100,7 @@ class SingleAsyncItemPager<T> {
private fun fillDeferredUntil(i: Int): Int { private fun fillDeferredUntil(i: Int): Int {
val startPos = _requestedPageItems.size; val startPos = _requestedPageItems.size;
for(i in _requestedPageItems.size..i) { for(v in _requestedPageItems.size..i) {
_requestedPageItems.add(CompletableDeferred()); _requestedPageItems.add(CompletableDeferred());
} }
return startPos; return startPos;
@@ -1,12 +1,17 @@
package com.futo.platformplayer.casting package com.futo.platformplayer.casting
import android.os.Looper import android.os.Looper
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.getConnectedSocket import com.futo.platformplayer.getConnectedSocket
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.toInetAddress import com.futo.platformplayer.toInetAddress
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.net.InetAddress import java.net.InetAddress
import java.util.UUID import java.util.UUID
@@ -18,7 +23,7 @@ class AirPlayCastingDevice : CastingDevice {
override var usedRemoteAddress: InetAddress? = null; override var usedRemoteAddress: InetAddress? = null;
override var localAddress: InetAddress? = null; override var localAddress: InetAddress? = null;
override val canSetVolume: Boolean get() = false; override val canSetVolume: Boolean get() = false;
override val canSetSpeed: Boolean get() = false; //TODO: Implement playback speed for AirPlay override val canSetSpeed: Boolean get() = true;
var addresses: Array<InetAddress>? = null; var addresses: Array<InetAddress>? = null;
var port: Int = 0; var port: Int = 0;
@@ -51,7 +56,8 @@ class AirPlayCastingDevice : CastingDevice {
Logger.i(FCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)"); Logger.i(FCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
time = resumePosition; setTime(resumePosition);
setDuration(duration);
if (resumePosition > 0.0) { if (resumePosition > 0.0) {
val pos = resumePosition / duration; val pos = resumePosition / duration;
Logger.i(TAG, "resumePosition: $resumePosition, duration: ${duration}, pos: $pos") Logger.i(TAG, "resumePosition: $resumePosition, duration: ${duration}, pos: $pos")
@@ -59,6 +65,10 @@ class AirPlayCastingDevice : CastingDevice {
} else { } else {
post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0"); post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0");
} }
if (speed != null) {
changeSpeed(speed)
}
} }
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) { override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
@@ -128,7 +138,7 @@ class AirPlayCastingDevice : CastingDevice {
try { try {
val connectedSocket = getConnectedSocket(adrs.toList(), port); val connectedSocket = getConnectedSocket(adrs.toList(), port);
if (connectedSocket == null) { if (connectedSocket == null) {
delay(3000); delay(1000);
continue; continue;
} }
@@ -153,16 +163,25 @@ class AirPlayCastingDevice : CastingDevice {
} }
connectionState = CastConnectionState.CONNECTED; connectionState = CastConnectionState.CONNECTED;
delay(1000);
val progressIndex = progressInfo.lowercase().indexOf("position: "); val progressIndex = progressInfo.lowercase().indexOf("position: ");
if (progressIndex == -1) { if (progressIndex == -1) {
delay(1000);
continue; continue;
} }
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue; val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
setTime(progress);
time = progress; val durationIndex = progressInfo.lowercase().indexOf("duration: ");
if (durationIndex == -1) {
delay(1000);
continue;
}
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
setDuration(duration);
delay(1000);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to get server info from AirPlay device.", e) Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
} }
@@ -186,6 +205,11 @@ class AirPlayCastingDevice : CastingDevice {
_scopeIO = null; _scopeIO = null;
} }
override fun changeSpeed(speed: Double) {
setSpeed(speed)
post("rate?value=$speed")
}
override fun getDeviceInfo(): CastingDeviceInfo { override fun getDeviceInfo(): CastingDeviceInfo {
return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port); return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port);
} }
@@ -1,7 +1,6 @@
package com.futo.platformplayer.casting package com.futo.platformplayer.casting
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.getNowDiffMiliseconds
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -11,7 +10,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import java.net.InetAddress import java.net.InetAddress
import java.time.OffsetDateTime
enum class CastConnectionState { enum class CastConnectionState {
DISCONNECTED, DISCONNECTED,
@@ -59,36 +57,58 @@ abstract class CastingDevice {
onPlayChanged.emit(value); onPlayChanged.emit(value);
} }
}; };
var timeReceivedAt: OffsetDateTime = OffsetDateTime.now()
private set; private var lastTimeChangeTime_ms: Long = 0
var time: Double = 0.0 var time: Double = 0.0
set(value) { private set
val changed = value != field;
field = value; protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changed) { if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
timeReceivedAt = OffsetDateTime.now(); time = value
onTimeChanged.emit(value); lastTimeChangeTime_ms = changeTime_ms
} onTimeChanged.emit(value)
}; }
}
private var lastDurationChangeTime_ms: Long = 0
var duration: Double = 0.0
private set
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
duration = value
lastDurationChangeTime_ms = changeTime_ms
onDurationChanged.emit(value)
}
}
private var lastVolumeChangeTime_ms: Long = 0
var volume: Double = 1.0 var volume: Double = 1.0
set(value) { private set
val changed = value != field;
field = value; protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changed) { if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
onVolumeChanged.emit(value); volume = value
} lastVolumeChangeTime_ms = changeTime_ms
}; onVolumeChanged.emit(value)
}
}
private var lastSpeedChangeTime_ms: Long = 0
var speed: Double = 1.0 var speed: Double = 1.0
set(value) { private set
val changed = value != field;
speed = value; protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changed) { if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
onSpeedChanged.emit(value); speed = value
} lastSpeedChangeTime_ms = changeTime_ms
}; onSpeedChanged.emit(value)
}
}
val expectedCurrentTime: Double val expectedCurrentTime: Double
get() { get() {
val diff = timeReceivedAt.getNowDiffMiliseconds().toDouble() / 1000.0; val diff = (System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0;
return time + diff; return time + diff;
}; };
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
@@ -104,6 +124,7 @@ abstract class CastingDevice {
var onConnectionStateChanged = Event1<CastConnectionState>(); var onConnectionStateChanged = Event1<CastConnectionState>();
var onPlayChanged = Event1<Boolean>(); var onPlayChanged = Event1<Boolean>();
var onTimeChanged = Event1<Double>(); var onTimeChanged = Event1<Double>();
var onDurationChanged = Event1<Double>();
var onVolumeChanged = Event1<Double>(); var onVolumeChanged = Event1<Double>();
var onSpeedChanged = Event1<Double>(); var onSpeedChanged = Event1<Double>();
@@ -2,17 +2,23 @@ package com.futo.platformplayer.casting
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.getConnectedSocket import com.futo.platformplayer.getConnectedSocket
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.protos.ChromeCast import com.futo.platformplayer.protos.ChromeCast
import com.futo.platformplayer.toHexString import com.futo.platformplayer.toHexString
import com.futo.platformplayer.toInetAddress import com.futo.platformplayer.toInetAddress
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.net.InetAddress import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocket
@@ -38,7 +44,9 @@ class ChromecastCastingDevice : CastingDevice {
private var _socket: SSLSocket? = null; private var _socket: SSLSocket? = null;
private var _outputStream: DataOutputStream? = null; private var _outputStream: DataOutputStream? = null;
private var _outputStreamLock = Object();
private var _inputStream: DataInputStream? = null; private var _inputStream: DataInputStream? = null;
private var _inputStreamLock = Object();
private var _scopeIO: CoroutineScope? = null; private var _scopeIO: CoroutineScope? = null;
private var _requestId = 1; private var _requestId = 1;
private var _started: Boolean = false; private var _started: Boolean = false;
@@ -46,6 +54,8 @@ class ChromecastCastingDevice : CastingDevice {
private var _transportId: String? = null; private var _transportId: String? = null;
private var _launching = false; private var _launching = false;
private var _mediaSessionId: Int? = null; private var _mediaSessionId: Int? = null;
private var _thread: Thread? = null;
private var _pingThread: Thread? = null;
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() { constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
this.name = name; this.name = name;
@@ -70,7 +80,8 @@ class ChromecastCastingDevice : CastingDevice {
Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)"); Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
time = resumePosition; setTime(resumePosition);
setDuration(duration);
_streamType = streamType; _streamType = streamType;
_contentType = contentType; _contentType = contentType;
_contentId = contentId; _contentId = contentId;
@@ -132,7 +143,7 @@ class ChromecastCastingDevice : CastingDevice {
return; return;
} }
this.volume = volume setVolume(volume)
val setVolumeObject = JSONObject(); val setVolumeObject = JSONObject();
setVolumeObject.put("type", "SET_VOLUME"); setVolumeObject.put("type", "SET_VOLUME");
@@ -265,7 +276,6 @@ class ChromecastCastingDevice : CastingDevice {
} }
override fun start() { override fun start() {
val adrs = addresses ?: return;
if (_started) { if (_started) {
return; return;
} }
@@ -278,152 +288,183 @@ class ChromecastCastingDevice : CastingDevice {
_launching = true; _launching = true;
_scopeIO?.cancel(); ensureThreadsStarted();
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.") Logger.i(TAG, "Started.");
_scopeIO = CoroutineScope(Dispatchers.IO); }
Thread { fun ensureThreadsStarted() {
connectionState = CastConnectionState.CONNECTING; val adrs = addresses ?: return;
while (_scopeIO?.isActive == true) { val thread = _thread
try { val pingThread = _pingThread
val connectedSocket = getConnectedSocket(adrs.toList(), port); if (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive) {
if (connectedSocket == null) { Log.i(TAG, "Restarting threads because one of the threads has died")
Thread.sleep(3000);
continue;
}
usedRemoteAddress = connectedSocket.inetAddress; _scopeIO?.cancel();
localAddress = connectedSocket.localAddress; Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
connectedSocket.close(); _scopeIO = CoroutineScope(Dispatchers.IO);
break;
} catch (e: Throwable) {
Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
}
}
val sslContext = SSLContext.getInstance("TLS"); _thread = Thread {
sslContext.init(null, trustAllCerts, null);
val factory = sslContext.socketFactory;
//Connection loop
while (_scopeIO?.isActive == true) {
Logger.i(TAG, "Connecting to Chromecast.");
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
try { var connectedSocket: Socket? = null
_socket?.close()
_socket = factory.createSocket(usedRemoteAddress, port) as SSLSocket;
_socket?.startHandshake();
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
try {
_outputStream = DataOutputStream(_socket?.outputStream);
_inputStream = DataInputStream(_socket?.inputStream);
} catch (e: Throwable) {
Logger.i(TAG, "Failed to authenticate to Chromecast.", e);
}
} catch (e: Throwable) {
_socket?.close();
Logger.i(TAG, "Failed to connect to Chromecast.", e);
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000);
continue;
}
localAddress = _socket?.localAddress;
try {
val connectObject = JSONObject();
connectObject.put("type", "CONNECT");
connectObject.put("connType", 0);
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.connection", connectObject.toString());
} catch (e: Throwable) {
Logger.i(TAG, "Failed to send connect message to Chromecast.", e);
_socket?.close();
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000);
continue;
}
getStatus();
val buffer = ByteArray(4096);
Logger.i(TAG, "Started receiving.");
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
try { try {
val inputStream = _inputStream ?: break; val resultSocket = getConnectedSocket(adrs.toList(), port);
Log.d(TAG, "Receiving next packet..."); if (resultSocket == null) {
val b1 = inputStream.readUnsignedByte(); Thread.sleep(1000);
val b2 = inputStream.readUnsignedByte();
val b3 = inputStream.readUnsignedByte();
val b4 = inputStream.readUnsignedByte();
val size = ((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
if (size > buffer.size) {
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
inputStream.skip(size.toLong());
continue; continue;
} }
Log.d(TAG, "Received header indicating $size bytes. Waiting for message."); connectedSocket = resultSocket
inputStream.read(buffer, 0, size); usedRemoteAddress = connectedSocket.inetAddress;
localAddress = connectedSocket.localAddress;
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
Logger.i(TAG, "Received message: $message");
}
try {
handleMessage(message);
} catch (e:Throwable) {
Logger.w(TAG, "Failed to handle message.", e);
}
} catch (e: java.net.SocketException) {
Logger.e(TAG, "Socket exception while receiving.", e);
break; break;
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Exception while receiving.", e); Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
break;
} }
} }
_socket?.close();
Logger.i(TAG, "Socket disconnected.");
connectionState = CastConnectionState.CONNECTING; val sslContext = SSLContext.getInstance("TLS");
Thread.sleep(3000); sslContext.init(null, trustAllCerts, null);
}
Logger.i(TAG, "Stopped connection loop."); val factory = sslContext.socketFactory;
connectionState = CastConnectionState.DISCONNECTED;
}.start();
//Start ping loop val address = InetSocketAddress(usedRemoteAddress, port)
Thread {
Logger.i(TAG, "Started ping loop.")
val pingObject = JSONObject(); //Connection loop
pingObject.put("type", "PING"); while (_scopeIO?.isActive == true) {
Logger.i(TAG, "Connecting to Chromecast.");
connectionState = CastConnectionState.CONNECTING;
while (_scopeIO?.isActive == true) { try {
try { _socket?.close()
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString()); if (connectedSocket != null) {
Thread.sleep(5000); Logger.i(TAG, "Using connected socket.")
} catch (e: Throwable) { _socket = factory.createSocket(connectedSocket, connectedSocket.inetAddress.hostAddress, connectedSocket.port, true) as SSLSocket
connectedSocket = null
} else {
Logger.i(TAG, "Using new socket.")
val s = Socket().apply { this.connect(address, 2000) }
_socket = factory.createSocket(s, s.inetAddress.hostAddress, s.port, true) as SSLSocket
}
_socket?.startHandshake();
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
try {
_outputStream = DataOutputStream(_socket?.outputStream);
_inputStream = DataInputStream(_socket?.inputStream);
} catch (e: Throwable) {
Logger.i(TAG, "Failed to authenticate to Chromecast.", e);
}
} catch (e: Throwable) {
_socket?.close();
Logger.i(TAG, "Failed to connect to Chromecast.", e);
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(1000);
continue;
}
localAddress = _socket?.localAddress;
try {
val connectObject = JSONObject();
connectObject.put("type", "CONNECT");
connectObject.put("connType", 0);
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.connection", connectObject.toString());
} catch (e: Throwable) {
Logger.i(TAG, "Failed to send connect message to Chromecast.", e);
_socket?.close();
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(1000);
continue;
}
getStatus();
val buffer = ByteArray(409600);
Logger.i(TAG, "Started receiving.");
while (_scopeIO?.isActive == true) {
try {
val inputStream = _inputStream ?: break;
synchronized(_inputStreamLock)
{
Log.d(TAG, "Receiving next packet...");
val b1 = inputStream.readUnsignedByte();
val b2 = inputStream.readUnsignedByte();
val b3 = inputStream.readUnsignedByte();
val b4 = inputStream.readUnsignedByte();
val size =
((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
if (size > buffer.size) {
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
inputStream.skip(size.toLong());
return@synchronized
}
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
inputStream.read(buffer, 0, size);
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
Logger.i(TAG, "Received message: $message");
}
try {
handleMessage(message);
} catch (e: Throwable) {
Logger.w(TAG, "Failed to handle message.", e);
}
}
} catch (e: java.net.SocketException) {
Logger.e(TAG, "Socket exception while receiving.", e);
break;
} catch (e: Throwable) {
Logger.e(TAG, "Exception while receiving.", e);
break;
}
}
_socket?.close();
Logger.i(TAG, "Socket disconnected.");
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(1000);
} }
}
Logger.i(TAG, "Stopped ping loop."); Logger.i(TAG, "Stopped connection loop.");
}.start(); connectionState = CastConnectionState.DISCONNECTED;
}.apply { start() };
Logger.i(TAG, "Started."); //Start ping loop
_pingThread = Thread {
Logger.i(TAG, "Started ping loop.")
val pingObject = JSONObject();
pingObject.put("type", "PING");
while (_scopeIO?.isActive == true) {
try {
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
} catch (e: Throwable) {
Log.w(TAG, "Failed to send ping.");
}
Thread.sleep(5000);
}
Logger.i(TAG, "Stopped ping loop.");
}.apply { start() };
} else {
Log.i(TAG, "Threads still alive, not restarted")
}
} }
private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) { private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) {
@@ -486,7 +527,7 @@ class ChromecastCastingDevice : CastingDevice {
if (!sessionIsRunning) { if (!sessionIsRunning) {
_sessionId = null; _sessionId = null;
_mediaSessionId = null; _mediaSessionId = null;
time = 0.0; setTime(0.0);
_transportId = null; _transportId = null;
Logger.w(TAG, "Session not found."); Logger.w(TAG, "Session not found.");
@@ -502,11 +543,11 @@ class ChromecastCastingDevice : CastingDevice {
} }
val volume = status.getJSONObject("volume"); val volume = status.getJSONObject("volume");
val volumeControlType = volume.getString("controlType"); //val volumeControlType = volume.getString("controlType");
val volumeLevel = volume.getString("level").toDouble(); val volumeLevel = volume.getString("level").toDouble();
val volumeMuted = volume.getBoolean("muted"); val volumeMuted = volume.getBoolean("muted");
val volumeStepInterval = volume.getString("stepInterval").toFloat(); //val volumeStepInterval = volume.getString("stepInterval").toFloat();
this.volume = if (volumeMuted) 0.0 else volumeLevel; setVolume(if (volumeMuted) 0.0 else volumeLevel);
Logger.i(TAG, "Status update received volume (level: $volumeLevel, muted: $volumeMuted)"); Logger.i(TAG, "Status update received volume (level: $volumeLevel, muted: $volumeMuted)");
} else if (type == "MEDIA_STATUS") { } else if (type == "MEDIA_STATUS") {
@@ -517,10 +558,16 @@ class ChromecastCastingDevice : CastingDevice {
val playerState = status.getString("playerState"); val playerState = status.getString("playerState");
val currentTime = status.getDouble("currentTime"); val currentTime = status.getDouble("currentTime");
if (status.has("media")) {
val media = status.getJSONObject("media")
if (media.has("duration")) {
setDuration(media.getDouble("duration"))
}
}
isPlaying = playerState == "PLAYING"; isPlaying = playerState == "PLAYING";
if (isPlaying) { if (isPlaying) {
time = currentTime; setTime(currentTime);
} }
val playbackRate = status.getInt("playbackRate"); val playbackRate = status.getInt("playbackRate");
@@ -548,13 +595,16 @@ class ChromecastCastingDevice : CastingDevice {
return; return;
} }
val serializedSizeBE = ByteArray(4); synchronized(_outputStreamLock)
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte(); {
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte(); val serializedSizeBE = ByteArray(4);
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte(); serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
serializedSizeBE[3] = (data.size and 0xff).toByte(); serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
outputStream.write(serializedSizeBE); serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
outputStream.write(data); serializedSizeBE[3] = (data.size and 0xff).toByte();
outputStream.write(serializedSizeBE);
outputStream.write(data);
}
//Log.d(TAG, "Sent ${data.size} bytes."); //Log.d(TAG, "Sent ${data.size} bytes.");
} }
@@ -582,6 +632,8 @@ class ChromecastCastingDevice : CastingDevice {
Logger.i(TAG, "Cancelled scopeIO without open socket.") Logger.i(TAG, "Cancelled scopeIO without open socket.")
} }
_pingThread = null;
_thread = null;
_scopeIO = null; _scopeIO = null;
_socket = null; _socket = null;
_outputStream = null; _outputStream = null;

Some files were not shown because too many files have changed in this diff Show More