Compare commits

...

567 commits

Author SHA1 Message Date
Kelvin
a882d04d26 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-08-01 21:56:00 +02:00
Kelvin
c4d06c1ba2 Hide sync ui, thumbnails nullable 2025-08-01 21:55:47 +02:00
Kelvin
4dfcd47901 Merge branch 'fix-null-thumbnails-error' into 'master'
fix: erroring out when thumbnails are null which causes sync to reset

See merge request videostreaming/grayjay!136
2025-07-31 16:37:29 +00:00
zvonimir
4c0c1abb4b fix: erroring out when thumbnails are null which causes sync to reset 2025-07-31 18:34:24 +02:00
Kelvin
6f44071186 Merge branch 'update-docs' into 'master'
docs: add section for Request Modifiers in Content types document

See merge request videostreaming/grayjay!134
2025-07-31 14:00:17 +00:00
Stefan
29910a2698 docs: add section for Request Modifiers in Content types document 2025-07-31 14:00:17 +00:00
Kelvin
b5da0d4462 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-30 18:22:27 +02:00
Kelvin
99fb9b3462 VOD chat support 2025-07-30 18:22:15 +02:00
Koen J
5f0a89d13b Implemented URLs open outside of live chat webview. 2025-07-30 13:46:46 +02:00
Koen
f311561e6f Merge branch 'fix-android-anr-swap-sources' into 'master'
Fix Android ANR in SwapSources.

See merge request videostreaming/grayjay!135
2025-07-29 09:51:15 +00:00
Koen J
2fc944ddd9 Cleanup. 2025-07-29 11:15:49 +02:00
Koen J
a2970b86ee Fixed issue where Scan QR button vanishes due to missing owner activity and fixed issue where remembered devices do not show until at least one normal device is found. 2025-07-29 10:58:41 +02:00
Kelvin
ac9a51f105 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-29 01:39:48 +02:00
Kelvin
90dca2537a getUserHistory support 2025-07-29 01:39:32 +02:00
Koen J
4df227147c Fix Android ANR in SwapSources. 2025-07-28 15:44:37 +02:00
Koen J
1fb55dca0a On casting device disconnect, only set play when ready to true if it was also playing on the TV device. 2025-07-24 13:44:04 +02:00
Koen J
3d7b347e49 Do not call playVideo on reconnects, but instead check MEDIA_STATUS. 2025-07-24 13:05:28 +02:00
Koen J
769ec9f59a Only auto relaunch player the first time ChromeCast is started, do not reset time to 0 if player is not found, stop casting if ChromeCast player is disconnected. 2025-07-24 12:23:23 +02:00
Koen J
dee310de3d Potential crashfix for downloads. 2025-07-24 11:16:48 +02:00
Koen
0af4bad906 Merge branch 'hls-url-redirect-fix' into 'master'
Nebula Download Fix

See merge request videostreaming/grayjay!129
2025-07-22 11:54:41 +00:00
Koen
4731673ba3 Merge branch 'fix-fullscreen-ui-offset' into 'master'
Fullscreen UI Fix

See merge request videostreaming/grayjay!116
2025-07-22 09:41:42 +00:00
Koen J
8745221cbd Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into fix-fullscreen-ui-offset 2025-07-22 11:39:20 +02:00
Koen
742d95440e Merge branch 'pip-improvement' into 'master'
implement the quick PiP feature

See merge request videostreaming/grayjay!107
2025-07-22 09:38:39 +00:00
Koen J
180b320cd7 Merge branch 'pip-improvement' of gitlab.futo.org:videostreaming/grayjay into pip-improvement 2025-07-22 11:31:27 +02:00
Koen J
cc8dffc485 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into pip-improvement 2025-07-22 11:30:35 +02:00
Koen
a64fd2cf35 Merge branch 'align-manifests' into 'master'
Align Manifests

See merge request videostreaming/grayjay!123
2025-07-21 15:07:27 +00:00
Koen
4aceb364d9 Edit AndroidManifest.xml 2025-07-21 15:06:57 +00:00
Koen J
76d9bac0ec Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-21 17:03:14 +02:00
Koen J
2b8dc41d0d Artwork should only show when audio mode is not transitioning out. 2025-07-21 17:03:00 +02:00
Koen
33430c538c Merge branch 'preview-feed-fix-constraints' into 'master'
Feed Preview Graphical Bug

See merge request videostreaming/grayjay!121
2025-07-21 14:01:37 +00:00
Koen J
03e9cb398b Made isLimitedVersion check more specific. 2025-07-21 15:54:48 +02:00
Koen J
2aef2ebec1 Background playback fixes for limited version and artwork now shows while in background playback. 2025-07-21 15:43:43 +02:00
Koen J
5e5fffbf97 loadPager should not be called on init. Small fix for restoring brightness when exiting app. 2025-07-21 14:57:03 +02:00
Koen J
51ac604e31 Various crash fixes. 2025-07-21 14:41:36 +02:00
Koen
4e49b5bc63 Merge branch 'shorts-tab' into 'master'
shorts tab

See merge request videostreaming/grayjay!92
2025-07-21 12:33:14 +00:00
Koen
658cbc5e00 Edit FCastCastingDevice.kt 2025-07-18 08:34:22 +00:00
Koen J
2ceb4c5644 Fixed issue where streams are not proxied when a request modifier is present. 2025-07-17 11:15:38 +02:00
Kai
2738954af7
add hard coded padding to compensate for this bug
https://gitlab.futo.org/videostreaming/grayjay/-/merge_requests/133

Changelog: changed
2025-07-11 15:40:01 -05:00
Kai
db5aaf0b84
fix delay when opening quality overlay
Changelog: changed
2025-07-11 13:50:32 -05:00
Kai
e1abb7f8ae
prevent scroll to top from showing when it shouldn't
fix zero state

Changelog: changed
2025-07-11 12:18:02 -05:00
Koen J
3310ac6008 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into shorts-tab 2025-07-11 10:16:21 +02:00
Kai
09879c83e9
fix sources button click
Changelog: changed
2025-07-10 10:45:24 -05:00
Kai
7aa8b6bc14
add zero state for shorts tab
Changelog: changed
2025-07-10 10:40:52 -05:00
Kai
cac8a8fde4
add back button when in channel shorts
hide refresh button when in channel shorts

prevent main player being open when viewing shorts

show info toast when long pressing refresh button

switch bottom bar button ids back to their original values

Changelog: changed
2025-07-10 09:30:42 -05:00
Kelvin
01cb544dfd Dont lock clients when disabling 2025-07-07 16:42:37 +02:00
Kelvin
b9239b6177 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 16:18:35 +02:00
Kelvin
96ca3f62a2 Missing invokev8 wrappers 2025-07-07 16:18:23 +02:00
Koen J
73ad783881 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 16:12:33 +02:00
Koen J
3bfcf65535 Fixes to reload required exception handling for casting. 2025-07-07 16:12:03 +02:00
Kelvin
8b3b27a2a8 Stop pagers silently if the underlying object is closed 2025-07-07 16:04:19 +02:00
Kelvin
a4d4835a89 Reduice font size 2025-07-07 14:26:17 +02:00
Kelvin
56c0f7bfaf Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 14:14:51 +02:00
Kelvin
736424ae35 Refs 2025-07-07 14:14:39 +02:00
Koen J
37dc778009 Fixed casting. 2025-07-07 12:45:45 +02:00
Koen J
cd3cea58a4 Fixed race condition when awaiting and changing video source.. 2025-07-07 10:52:42 +02:00
Koen J
8b53e9e5e3 Processed last feedback on minigame. 2025-07-05 18:09:49 +02:00
Koen J
08e98b089c Improvements to target tap loader game. 2025-07-05 17:32:31 +02:00
Koen J
5528d71da8 Show score toast. 2025-07-05 14:07:49 +02:00
Koen J
83f520ca44 Further fixes to TargetTApLoaderView. 2025-07-05 13:47:48 +02:00
Koen J
cc247ce634 Attempt at a loader game. 2025-07-05 12:58:33 +02:00
Kelvin
c6caa59a90 Refs 2025-07-04 18:00:48 +02:00
Kelvin
00e28b9ce0 Better raid messaging, loader autochange to indeterminate, refs 2025-07-04 17:27:35 +02:00
Kelvin
334f58979a Add missing file 2025-07-04 16:13:11 +02:00
Kelvin
940bf163da Progress bar color, refs 2025-07-04 15:58:46 +02:00
Kelvin
2bbe0e6133 Substitute v8 object calls to wrapper function 2025-07-04 15:24:19 +02:00
Kelvin
861f34a287 Refs 2025-07-04 15:02:01 +02:00
Koen J
82f214f155 Merge branch 'shorts-tab' of gitlab.futo.org:videostreaming/grayjay into shorts-tab 2025-07-04 08:16:34 +02:00
Koen J
4ee127fe13 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into shorts-tab 2025-07-04 08:16:12 +02:00
Koen J
86a4cf8d84 Implemented FutoVideoPlayer loader. 2025-07-04 08:09:24 +02:00
Kelvin
2c463dd5a1 Merge branch 'wip-async' into 'master'
Basic Async support

See merge request videostreaming/grayjay!132
2025-07-03 17:19:27 +00:00
Kelvin
ed3820bec0 Finish async generates 2025-07-03 19:15:14 +02:00
Kelvin
542a7f212d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into wip-async 2025-07-03 19:05:38 +02:00
Kelvin
8fb0826d69 WIP rewrite generate to async 2025-07-03 19:05:13 +02:00
Koen J
deeaa55f56 Changed default behavior for isOutgoing. 2025-07-03 14:40:27 +02:00
Koen J
5b954727a1 Implemented incoming and outgoing raids. 2025-07-03 14:30:15 +02:00
Koen
fae77c1a63 Merge branch 'rgba-colors' into 'master'
RGBA colors.

See merge request videostreaming/grayjay!131
2025-07-03 07:44:19 +00:00
Kelvin
b69402dfe9 WIP Async support for Android 2025-07-03 00:44:54 +02:00
Koen J
1f3e306a59 RGBA colors. 2025-07-02 17:59:13 +02:00
Koen J
a9605118fb Clip to outline does not make sense for a ShapeableImageView. 2025-07-01 10:02:06 +02:00
Kelvin
d22e918273 Missing catches 2025-06-26 16:17:17 +02:00
Kelvin
bdcb94055a Refs 2025-06-26 15:13:41 +02:00
Kelvin
d0644d39da Theoretical fix for networked file import 2025-06-26 15:01:18 +02:00
Kelvin
8f3f776e22 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-26 14:35:30 +02:00
Kelvin
548752e240 missing lock 2025-06-26 14:35:00 +02:00
Koen J
7f20250951 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-24 11:43:59 +02:00
Koen J
4d720b1d81 Fixed app freezing when exporting Polycentric Identity #2405 2025-06-24 11:43:40 +02:00
Kai
1e4aefb7d5
fix https://github.com/futo-org/grayjay-android/issues/2386
Changelog: changed
2025-06-20 12:00:56 -05:00
Kai
2a825a9f83
platform icon
fix text shadow

Changelog: changed
2025-06-20 09:55:39 -05:00
Kelvin K
a8921a1aba Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-20 15:35:30 +02:00
Kelvin K
edb9eda0a9 Improved locking 2025-06-20 15:35:02 +02:00
Koen J
3a81676447 Fixed crash #2389. 2025-06-20 10:47:10 +02:00
Kai
6695774037
icon updates
Changelog: changed
2025-06-19 11:14:00 -05:00
Koen J
03132ff77b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-19 11:23:28 +02:00
Koen J
49ddecdea4 Potential crashfix #2382. 2025-06-19 11:21:46 +02:00
Kai
a10bc8c7de
fix very wide screen videos enter PiP mode
Changelog: changed
2025-06-18 12:12:39 -05:00
Kai
c1e6e401cc
formatting
Changelog: changed
2025-06-18 09:36:31 -05:00
Kelvin K
44ff951ec6 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 16:22:51 +02:00
Kelvin K
11319e0ec5 Refs 2025-06-18 16:22:28 +02:00
Koen J
100e98a960 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 15:43:45 +02:00
Koen J
c6100ede70 Added disable for hold playback rate increase. 2025-06-18 15:43:12 +02:00
Kelvin K
a2986a72bd Refs 2025-06-18 14:43:20 +02:00
Kelvin K
e0e90c5f74 submodules 2025-06-18 14:33:07 +02:00
Kelvin K
11992af81b Hide duration if unknown 2025-06-18 14:27:20 +02:00
Koen J
15d771f7fc Fixed channel loader not being animated. 2025-06-18 13:43:50 +02:00
Kelvin K
5ede474253 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 12:41:43 +02:00
Kelvin K
7922aa6f80 Log on busy on main 2025-06-18 12:41:21 +02:00
Kelvin K
0c1333fa15 Downgrade v8, revert comments on diff thread 2025-06-18 12:40:25 +02:00
Koen J
53b9ba0368 Reverted changes. 2025-06-18 10:29:12 +02:00
Koen J
c3a8877796 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 10:07:29 +02:00
Koen J
a464ae9df5 Added missing loader causing crash. 2025-06-18 10:07:02 +02:00
Kai
98b6213886
remove layout changed listener
Changelog: changed
2025-06-17 15:26:27 -05:00
Kai
b6671c653c
add text shadow
add HQ icon for quality settings

Changelog: changed
2025-06-17 13:45:15 -05:00
Kai
55d042bee3
add platform logo
Changelog: changed
2025-06-17 12:07:29 -05:00
Kelvin K
0d16dd0006 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-17 17:28:17 +02:00
Kelvin K
48a96140a7 isBusy checks and locking improvements 2025-06-17 17:28:10 +02:00
Kelvin
603ef8f295 Merge branch 'fix-timeout-locking' into 'master'
fix: timeoutMap being deadlocked

See merge request videostreaming/grayjay!126
2025-06-17 15:26:51 +00:00
zvonimir
ab07288ba0 fix: timeoutMap being deadlocked 2025-06-17 17:25:34 +02:00
Kelvin K
c0bbe5d491 Additional locking 2025-06-17 15:21:46 +02:00
Kelvin K
b953ff21e7 Lock on subtitle fetch 2025-06-17 11:52:26 +02:00
Kelvin K
c14378b534 Improved V8 locking, comment section on diff thread than video, global mapping of v8runtimes to plugins 2025-06-17 11:45:02 +02:00
Kai
80034ad131
update refresh text
Changelog: changed
2025-06-16 13:25:02 -05:00
Kelvin K
33d3d9a29c Improved locking 2025-06-16 19:30:52 +02:00
Kelvin K
7e83793586 Submods 2025-06-16 18:34:37 +02:00
Kelvin K
6ba9ec8bc2 Clearer name setting 2025-06-16 17:56:04 +02:00
Kelvin
0b02ab0e2d Merge branch 'plugin-fixes' into 'master'
V8 Update, V8 interaction locking, Package fixes, ReloadRequiredException support

See merge request videostreaming/grayjay!125
2025-06-16 15:48:01 +00:00
Kelvin K
ff531b5e77 Cleanup, fixes, clearCookies support on httpClients 2025-06-16 17:46:00 +02:00
Kelvin K
b3f9de3b83 edgecase fix 2025-06-16 14:23:34 +02:00
Kelvin K
86bd71b89c Fix edgecase 2025-06-16 14:19:23 +02:00
Kelvin K
2fca7e9a01 Locking of most known v8 interactions, fix returning previously returned jvm objects, Related fixes 2025-06-16 14:13:47 +02:00
Koen
2cc873ef60 Merge branch 'quality-selector-fix' into 'master'
fix graphical glitches with quality selector

See merge request videostreaming/grayjay!109
2025-06-16 10:07:16 +00:00
Koen
7a66ce6bcd Merge branch 'sources-tab-scrolling-fix' into 'master'
Sources Scrolling Fix

See merge request videostreaming/grayjay!114
2025-06-16 10:01:45 +00:00
Koen
2730569b6b Merge branch 'tablet-landscape-fix' into 'master'
Tablet Landscape Fix

See merge request videostreaming/grayjay!115
2025-06-16 09:57:57 +00:00
Koen
ede5c4409c Merge branch 'watch-later-add-feature' into 'master'
Water Later Add Feature

See merge request videostreaming/grayjay!117
2025-06-16 09:54:43 +00:00
Koen
0dbe398435 Merge branch 'hls-quality-sort' into 'master'
Adaptive Quality Sort

See merge request videostreaming/grayjay!118
2025-06-16 09:41:22 +00:00
Koen J
bcab3bccbc Fixed crash when signature fields are wrongly populated. 2025-06-16 10:43:57 +02:00
Kelvin K
58c9aeb1a2 WIP: V8 update, package http fixes, ReloadRequiredException support, other fixes. Currently broken in situations where setTimeout is used 2025-06-14 15:51:31 +02:00
Kelvin K
4702787784 WIP 2025-06-13 17:47:22 +02:00
Kai
30c41044da
Merge branch 'master' into pip-improvement
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
2025-06-13 08:45:09 -05:00
Kai
e369676808
remove spotify.com because those are not handled by the spotify plugin
Changelog: changed
2025-06-12 14:17:56 -05:00
Kai
2fa9e65bee
align stable and unstable manifests
Changelog: changed
2025-06-12 14:13:08 -05:00
Kai
cf96bd1ec0
Add functionality to open channel shorts in shorts fragment
Changelog: changed
2025-06-12 14:01:11 -05:00
Kai
1f5a069877
Fix like button checked when not logged into polycentric
Open video details when tapping title

Changelog: changed
2025-06-12 10:42:08 -05:00
Koen J
13100dc38d Minor fix in playback speed setting. 2025-06-12 11:21:00 +02:00
Koen J
5227041398 Added setting for hold playback speed increase. Implemented chromecast playback rate adjustment in range [1, 2]. Implemented hold playback speed increase pill. 2025-06-12 10:33:05 +02:00
Kelvin
8491d4da1a Merge branch 'fix-ump-downloads' into 'master'
Revert downloads patch which broke downloads

See merge request videostreaming/grayjay!122
2025-06-11 16:41:20 +00:00
zvonimir
9bea1563ca Revert downloads patch which broke downloads 2025-06-11 18:36:05 +02:00
Kai
adc5013ea4
Fix the constraints on the feed preview items
Changelog: changed
2025-06-11 10:30:48 -05:00
Koen J
9e7b936663 Implemented hold to play video at 2x speed gesture. 2025-06-11 17:03:53 +02:00
Kelvin
19c84475db Hotfix playback speed for non-dot locales 2025-06-10 23:27:01 +02:00
Kai
515c5e00e9
Fix live stream PiP mode
Prevent splash screen when opening PiP mode

Changelog: changed
2025-06-10 15:27:14 -05:00
Kelvin
4164b1a3f8 Build fix 2025-06-10 19:25:43 +02:00
Kelvin
a9dc038190 Build fix 2025-06-10 19:20:50 +02:00
Kelvin
2825db88a5 Minor playback tracker fix, submodules 2025-06-10 18:56:19 +02:00
Kelvin
363099b303 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-10 17:39:44 +02:00
Kelvin
5e25a5054f Increase max comment length, Fix raw dash downloads ending too early, Fix playback tracker not working for downloaded videos 2025-06-10 17:33:14 +02:00
Kelvin
2bc6127f6b Merge branch 'copy-title' into 'master'
Copy Title

See merge request videostreaming/grayjay!110
2025-06-10 14:41:49 +00:00
Kelvin
064824aedf Merge branch 'copy-playlists' into 'master'
Clone Playlist

See merge request videostreaming/grayjay!111
2025-06-10 14:41:07 +00:00
Kai DeLorenzo
52044edb2e Merge branch 'brightness-fix' into 'master'
Dim Fullscreen Fix

See merge request videostreaming/grayjay!119
2025-06-10 14:38:01 +00:00
Kai
fb12073a82
Only save brightness on resume fullscreen if use system brightness is enabled
Changelog: changed
2025-06-10 09:18:28 -05:00
Kai
9944842a2f
Change adaptive streaming (HLS and Dash) quality to sort in descending quality to align with YouTube and the rest of Grayjay
Changelog: changed
2025-06-09 17:02:55 -05:00
Kai
99dc50894c
update text
Changelog: changed
2025-06-09 16:54:24 -05:00
Kelvin
de39451f67 Merge 2025-06-07 16:44:57 +02:00
Kelvin
8f28653b28 Fix edgecases for new playback speed control 2025-06-07 16:44:20 +02:00
Kai
6598dff6df
add add to watch later setting
add https://github.com/futo-org/grayjay-android/issues/2173

Changelog: added
2025-06-06 23:35:59 -05:00
Kai
389798457b
navigate to playlist screen after copying
Changelog: changed
2025-06-06 15:57:09 -05:00
Kai
ba9f843368
fix https://github.com/futo-org/grayjay-android/issues/2165
Changelog: changed
2025-06-06 15:45:12 -05:00
Kai
623c47fa2e
fix https://github.com/futo-org/grayjay-android/issues/2210
Changelog: changed
2025-06-06 15:25:46 -05:00
Kai
19861fe812
fix https://github.com/futo-org/grayjay-android/issues/2316
Changelog: changed
2025-06-06 13:40:20 -05:00
Kai
dd1c04bea1
make the copied playlist name unique
Changelog: changed
2025-06-06 09:39:09 -05:00
Kelvin
e6159117f6 Merge branch 'fix-scope-issue' into 'master'
fix: Scope getting removed when switching between settings 'Kelvin approved'

See merge request videostreaming/grayjay!113
2025-06-06 13:42:49 +00:00
zvonimir
0d9e1cd3c5 fix: Scope getting removed when switching between settings 'Kelvin approved' 2025-06-06 15:40:43 +02:00
Koen J
10753eb879 Sort to prefer ipv4 over ipv6. 2025-06-06 12:25:25 +02:00
Koen J
29aec21095 Merge branch 'hotfix-250606' of gitlab.futo.org:videostreaming/grayjay 2025-06-06 12:17:49 +02:00
Koen J
a810f82ce2 Added boolean setting to allow link local casting over ipv4. 2025-06-06 11:21:41 +02:00
Koen J
2c454a0ec5 Added boolean setting to allow link local casting over ipv4. 2025-06-06 11:20:04 +02:00
Koen J
d3dca00482 Merge branch 'hotfix-250606' of gitlab.futo.org:videostreaming/grayjay 2025-06-06 11:12:58 +02:00
Koen J
d08dffd9e2 Added potential fix for having to restart app to get casting devices to show. Added persistent ordering for creators. 2025-06-06 11:12:31 +02:00
Koen J
5b50ac926e Freeze fix when clicking link in description. 2025-06-06 10:17:40 +02:00
Koen J
57a3be35d0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-06 10:17:03 +02:00
Koen J
70f36e69e6 Freeze fix when clicking link in description. 2025-06-06 10:15:15 +02:00
Kai
8e70f1b865
add long tap to copy playing video title
Changelog: added
2025-06-05 23:14:03 -05:00
Kai
f86fb0ee44
add functionality to copy playlists
fix https://github.com/futo-org/grayjay-android/issues/2306

Changelog: added
2025-06-05 23:13:05 -05:00
Kelvin
fe0aac7c6e WIP playback speed additions 2025-06-05 22:47:45 +02:00
Kelvin
b93447f712 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-05 19:12:10 +02:00
Kelvin
84a5103526 Use lifecycle scope instead of root scope 2025-06-05 19:11:55 +02:00
Kai
c333300906
fix graphical glitches with quality selector
Changelog: changed
2025-06-05 11:08:19 -05:00
Koen
c94c2721d7 Revert "prevent the user from needing to tap update on system dialog when self updating"
This reverts commit a1d460385d
2025-06-05 15:14:31 +00:00
Koen J
0428c1191a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-05 15:19:00 +02:00
Koen J
8208f92802 Added view license in settings. 2025-06-05 15:18:45 +02:00
Koen
3d33c4b8e0 Merge branch 'github-issues-template' into 'master'
Improve android issue templates

See merge request videostreaming/grayjay!108
2025-06-05 12:22:49 +00:00
zvonimir
d3210ec12a Improve android issue templates 2025-06-05 14:15:00 +02:00
Koen J
c959b973fc Crashfix related to PiP #2041. 2025-06-05 13:17:15 +02:00
Koen J
40c195d4a0 Crashfix on stopping StateSync #2302 2025-06-05 13:14:57 +02:00
Koen J
f4f1470153 Increased connect timeout. 2025-06-05 10:58:32 +02:00
Koen J
401999b5ea Fixed exception in sync. 2025-06-05 10:45:36 +02:00
Koen J
7b53315046 Another fix for connection robustness. 2025-06-05 10:38:49 +02:00
Koen J
4d170db5e0 Improvements to connection publishing for sync. 2025-06-05 10:31:13 +02:00
Koen J
fa8d175101 Fixed issue in base64 encoding. 2025-06-05 09:58:10 +02:00
Koen J
cbef605f22 Updated plugins. 2025-06-05 08:57:33 +02:00
Koen J
cf95791dcc Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-05 08:53:19 +02:00
Koen J
919567dbdb Made sync dialogs more robust. 2025-06-05 08:52:59 +02:00
Kai DeLorenzo
8ca317a38a Merge branch 'playback-stutter-fix' into 'master'
background playback stutter fix

See merge request videostreaming/grayjay!103
2025-06-04 20:44:09 +00:00
Kelvin
ccc686ed50 Downloads size ordering, Subsgroup removal on unsubscribe, multi-key like querying 2025-06-04 21:26:41 +02:00
Kelvin
e3e7b0c345 More advanced settings 2025-06-04 20:50:10 +02:00
Kelvin
5b0f359944 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-04 20:43:59 +02:00
Kelvin
29f1bef099 Advanced settings option, Playlist id saving for exports and backups, Sync synchronization to prevent dups 2025-06-04 20:43:37 +02:00
Kai
0653f88c49
Merge branch 'master' into shorts-tab
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
2025-06-04 13:03:40 -05:00
Kai
4ce9f64808
make code review changes
switch to TaskHandler
switch to XML

Changelog: changed
2025-06-04 12:59:48 -05:00
Koen J
418f34c7e8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-04 15:13:42 +02:00
Koen J
21c2ab21b2 Disable drag long press for search playlists. 2025-06-04 15:13:25 +02:00
Kelvin
1ace7318f3 Submodules 2025-06-04 13:17:31 +02:00
Koen J
48052b88db Made task handler and retry dialogs more robust. 2025-06-04 13:00:32 +02:00
Koen J
715c60dc6e Fixed Chromecast position not updating on Grayjay side. Fixed Chromecast not reconnecting properly. Fixed AirPlay/Chromecast position not being reflected in history. 2025-06-04 12:13:18 +02:00
Koen J
916d052688 Updated submodules. 2025-06-04 11:04:00 +02:00
Koen J
993b812c3b Removed drag handles when searching in playlist. 2025-06-04 10:44:00 +02:00
Koen J
43887586b5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-30 18:39:45 +02:00
Koen J
03d53f21a3 Tab changes no longer fill up history. 2025-05-30 18:39:28 +02:00
Kelvin
23d7e8e5b6 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-30 17:20:28 +02:00
Kelvin
cce117c585 Article header support, socket handling improvements 2025-05-30 17:20:11 +02:00
Koen J
303bd1b805 Fixed spacing issue in bottom bar when button sizes vary. Potential fix for crash that happens in ShapeableImageView. 2025-05-30 14:11:05 +02:00
koen-futo
c7f4a40342
Merge pull request #2288 from quonverbat/typo-fix
Fixed typo
2025-05-30 13:50:56 +02:00
quonverbat
208c6c0776 Fixed typo 2025-05-30 00:50:22 +03:00
Kai
4fa0229ccb
implement the quick PiP feature
https://developer.android.com/develop/ui/views/picture-in-picture#setautoenterenabled

Changelog: changed
2025-05-29 12:38:37 -05:00
Kelvin
7d5c8347ce Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-29 18:37:29 +02:00
Kelvin
bd70131252 Wrapper fix for unsupported enum parameters 2025-05-29 18:37:12 +02:00
Kelvin
43a373eceb Merge branch 'article-web-support' into 'master'
Article/web support

See merge request videostreaming/grayjay!106
2025-05-29 16:08:04 +00:00
Kelvin
5bb3466ffe Web support, additional article support 2025-05-29 18:02:55 +02:00
Kai
75e97ed008
Merge remote-tracking branch 'origin/master' 2025-05-29 10:24:51 -05:00
Kai
ee28604c11
switch audio HLS to m4a
Changelog: changed
2025-05-29 10:24:23 -05:00
Kelvin
a7d89e1bfb Missing files 2025-05-29 16:51:31 +02:00
Kelvin
cbfd9ea559 Article support 2025-05-29 16:51:06 +02:00
Koen J
dae50c3bc3 Implemented navigateWhenReady for url handling. 2025-05-29 16:07:14 +02:00
Koen J
e651e59dc4 Theoretical crashfix for the case where activity does not successfully resume before handleUrl is invoked #2245. 2025-05-29 15:39:46 +02:00
Koen J
80d78761bf Made chromecast more robust. Improvements to IPv6 handling of casting devices. 2025-05-29 15:17:26 +02:00
Kelvin
fb85aa4f32 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into article-web-support 2025-05-29 13:05:09 +02:00
Kelvin
9635c95efe WIP article 2025-05-29 13:05:00 +02:00
Koen J
033a237488 Changed getLocalUrl logic. 2025-05-29 13:03:51 +02:00
Koen J
ec22c58822 Made a fix for ui mode changes causing app restarts. 2025-05-29 11:58:10 +02:00
Koen J
274942b5ba Hide search for any tab that isn't videos. 2025-05-29 11:55:21 +02:00
Koen J
94ab3da0e4 Added separate error status code for transport rejection. Added unhandled exception handler for relay loop. Added additional booleans to keep track of the server/relay connections being up/down. Added additional messaging to let the user know when something is wrong. 2025-05-29 11:51:59 +02:00
Kai DeLorenzo
5d44f0f2b6 Merge branch 'download-encrypted-hls' into 'master'
Encrypted HLS Download

See merge request videostreaming/grayjay!89
2025-05-29 04:20:54 +00:00
Kai
f051e6b452
Merge branch 'master' into download-encrypted-hls
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/parsers/HLS.kt
2025-05-28 23:20:22 -05:00
Kai DeLorenzo
46a4284253 Merge branch 'hls-audio-fixes' into 'master'
HLS audio download

See merge request videostreaming/grayjay!86
2025-05-29 04:17:55 +00:00
Kai
0a708c6892
Merge branch 'master' into hls-audio-fixes
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
2025-05-28 23:12:50 -05:00
Kai DeLorenzo
0f96164dc3 Merge branch 'small-hls-fixes' into 'master'
Fix HLS downloading for nebula

See merge request videostreaming/grayjay!85
2025-05-29 04:10:06 +00:00
Kai
91c4917021
Merge branch 'master' into download-encrypted-hls
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
2025-05-28 22:55:09 -05:00
Kai
c32ebe016b
fix extension
Changelog: changed
2025-05-28 17:33:35 -05:00
Kai
ea26eefc2d
Merge branch 'master' into hls-audio-fixes 2025-05-28 17:24:32 -05:00
Kai
418f4a6075
Merge branch 'master' into small-hls-fixes 2025-05-28 16:57:57 -05:00
Koen J
0ec921709a Fixed search visibility and channel loader when changing query. 2025-05-28 20:32:09 +02:00
Koen J
e0811cfd93 Implemented new channel search. 2025-05-28 20:08:22 +02:00
Kai DeLorenzo
f6b0778eb6 Merge branch 'ffmpeg-build-fix' into 'master'
add smart exception dependency for usage by local ffmpegkit dependency

See merge request videostreaming/grayjay!105
2025-05-27 19:06:50 +00:00
Kai
18aec34c0e
add smart exception dependency for usage by local ffmpegkit dependency
Changelog: added
2025-05-27 14:05:51 -05:00
Kelvin
bd185776e7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into article-web-support 2025-05-27 15:49:27 +02:00
Kelvin
fca5fe38bb WIP Article/WEb 2025-05-27 15:49:20 +02:00
koen-futo
1c2c7b376d
Merge pull request #2152 from deenle/master
Added adaptive monochrome launcher icon
2025-05-27 11:47:33 +02:00
Koen J
670df86114 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-27 11:47:08 +02:00
Koen
55fb4d4562 Merge branch 'add-crunchyroll' into 'master'
Add Crunchyroll

See merge request videostreaming/grayjay!104
2025-05-26 12:59:15 +00:00
Koen J
c703d018bd Fixed issue where loop video would not reset when opening a playlist, causing the video to loop without being able to disable it. 2025-05-26 14:22:49 +02:00
Stefan
425a27e130 Add Crunchyroll 2025-05-26 00:47:54 +01:00
Kai
bd1b0e875b
Merge branch 'master' into hls-audio-fixes
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
#	app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
#	app/src/main/java/com/futo/platformplayer/parsers/HLS.kt
2025-05-22 15:04:15 -05:00
Kai
1509c11f64
check to see if an HLS playlist is a master playlist before parsing it
Changelog: changed
2025-05-22 15:00:34 -05:00
Kai
57c1097fbc
Merge branch 'master' into small-hls-fixes
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
2025-05-22 14:02:28 -05:00
Kai
1d1728b92b
switch audio hls to be an mp4 file
Changelog: changed
2025-05-22 13:44:50 -05:00
Kai
8202513993
fix stutter when switching to background
Changelog: changed
2025-05-22 12:12:34 -05:00
Kai DeLorenzo
5f9f6dbde8 Merge branch 'linked-channel-shorts-fix' into 'master'
Polycentric Channel Shorts Tab Fix

See merge request videostreaming/grayjay!100
2025-05-22 16:32:27 +00:00
Kai
cc3639180b
Merge branch 'master' into linked-channel-shorts-fix
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt
2025-05-22 11:31:05 -05:00
Kai
8aa4de7522
make sure the plugin supports shorts content
Changelog: added
2025-05-22 11:28:59 -05:00
Koen J
ed1f7e7c72 Button incognito is now visible but translated when minimized. 2025-05-22 15:57:12 +02:00
Koen J
1ecd1f5e04 Fix for throttled networks (airplane wifi) freezing app opening downloaded content. 2025-05-22 13:45:57 +02:00
Koen J
1aa9adc899 Build fix. 2025-05-22 11:41:16 +02:00
Koen J
f8b2da93b9 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-22 09:36:16 +02:00
Koen J
b794ff47ef Added dependency. 2025-05-22 09:34:32 +02:00
Kai DeLorenzo
6962a0547a Merge branch 'fix-get-replies' into 'master'
Fix getReplies in source.js

See merge request videostreaming/grayjay!102
2025-05-21 21:21:38 +00:00
Kai
b906c1d36b
add assignment for getReplies to PlatformComment
Changelog: added
2025-05-21 16:20:35 -05:00
Koen J
af337b1874 Added missing checks for polycentric enabled. 2025-05-19 15:37:39 +02:00
Koen J
542235cca0 Keep live chat open when minimizing. #2227 2025-05-19 11:45:45 +02:00
Koen J
f5673425b7 Updated default price. 2025-05-17 08:54:40 +02:00
Koen J
94965cf3ba Use snapshots for sending order to improve send efficiency. 2025-05-16 12:34:48 +02:00
Koen J
120ded5274 Prepared Sync logic to be separated from the rest of the logic. 2025-05-16 12:11:41 +02:00
Koen J
705eb6a3fa Migrated service to SyncService. 2025-05-15 19:57:31 +02:00
Koen J
1eb62b31d2 Added client sided ping loops to prevent inactive channels. 2025-05-13 10:31:30 +02:00
koen-futo
b145187fa8
Merge pull request #1803 from ajp-dev/patch-1
fix link typo for Script Signing
2025-05-12 09:47:51 +02:00
koen-futo
4da1e44fd1
Merge pull request #1724 from AlexandrePicavet/feat/player-seek-offset
feat(player): Add a setting to adjust player seek duration
2025-05-12 09:43:22 +02:00
koen-futo
4e70279982
Merge pull request #2136 from buzzcola3/master
Fix "previous song" button playing always from unshuffled queue, even when shuffle is used
2025-05-12 09:36:52 +02:00
koen-futo
233c8ee26e
Merge pull request #2155 from realchrisolin/patch-1
Update AndroidManifest.xml
2025-05-12 09:24:28 +02:00
Koen
875adb4d79 Merge branch 'fix-other-cookie-handling' into 'master'
Pass httponly cookies to future requests

See merge request videostreaming/grayjay!88
2025-05-12 07:01:41 +00:00
Koen
456514c4d4 Merge branch 'hls-rumble-fix' into 'master'
Rumble Download HLS Fix

See merge request videostreaming/grayjay!95
2025-05-12 06:46:55 +00:00
Koen J
dac1918b95 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-12 08:43:12 +02:00
Koen J
1d7429ad86 Fixed issue where private mode would not re-appear after closing the video. 2025-05-12 08:43:02 +02:00
Kai DeLorenzo
5d0e6615ab Edit VideoDownload.kt 2025-05-09 17:36:27 +00:00
Kai DeLorenzo
dc415df8c0 Edit VideoDownload.kt 2025-05-09 17:35:36 +00:00
Kai DeLorenzo
45ce251c4c Merge branch 'streamline-self-update' into 'master'
Silent Self Update

See merge request videostreaming/grayjay!81
2025-05-09 17:10:47 +00:00
Koen
2bc702112f Merge branch 'fix-brightness-restore' into 'master'
Brightness Restore On App Close

See merge request videostreaming/grayjay!99
2025-05-09 16:18:41 +00:00
Kai DeLorenzo
abd73bf797 Merge branch 'theme-change-restart' into 'master'
Fix Restart On System Theme Change

See merge request videostreaming/grayjay!97
2025-05-09 15:17:52 +00:00
Kai
e7e67b9572
organize imports
Changelog: changed
2025-05-09 10:17:15 -05:00
Kai
1a58b693c1
Merge branch 'master' into theme-change-restart
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
2025-05-09 10:14:33 -05:00
Kai
50ecb909b4
Remove cookie
Changelog: changed
2025-05-09 10:08:24 -05:00
Koen
5e480be8db Merge branch 'fullscreen-glitch-fix' into 'master'
Fullscreen UI Glitch Fix

See merge request videostreaming/grayjay!101
2025-05-09 06:43:40 +00:00
Koen J
48a67e51a6 Various fixes. 2025-05-07 20:57:55 +02:00
Koen J
5052bad824 Updated submodules. 2025-05-07 15:46:07 +02:00
Koen J
5be92052bb Fixed device vanishing while still being there in the case the device is blacklisted from starting a relay. 2025-05-07 15:43:28 +02:00
Koen
e20945692e Merge branch 'add-plugin-curiosity-stream' into 'master'
add Curiosity Stream plugin

See merge request videostreaming/grayjay!94
2025-05-07 13:03:35 +00:00
Stefan
191a6e2460 add Curiosity Stream plugin 2025-05-07 13:03:35 +00:00
Koen J
c813fb4fad Do not show history entries where it could not retrieve a plugin id. 2025-05-07 14:34:57 +02:00
Koen J
bf7001b578 Implemented to background button in pip. 2025-05-07 14:17:29 +02:00
Koen J
18102a2a73 Try a more heavy handed approach to get plugin ids for history changes. 2025-05-07 13:26:31 +02:00
Koen J
780c1dbde1 Reverted platform filters on history page. 2025-05-07 13:18:22 +02:00
Koen J
879aab0d99 Added progress bar to playlist items. 2025-05-07 13:08:13 +02:00
Koen J
6f37bc2f5d Confirming sync now brings you back to the device list. 2025-05-07 12:57:44 +02:00
Koen J
fc59b841d6 History now filters out videos of plugins that are not enabled. History now has a list of filters to filter specific plugins. History now shows an icon of which platform a specific history video is on. 2025-05-07 12:49:39 +02:00
Koen J
c07fcdd489 Prevent going into picture in picture when clicking add sources. 2025-05-07 11:22:54 +02:00
Koen J
a49db10ade Fixed issue 2163. 2025-05-07 11:01:25 +02:00
Koen J
77bae98d77 Private mode visibility no longer overlays pip/minimized video. 2025-05-07 10:57:39 +02:00
Koen J
254df7211c Fixed datetime checking related to playlists on android. 2025-05-07 10:36:32 +02:00
Koen J
f9caab48c4 Added insensitivity to base64 formats. 2025-05-07 10:12:12 +02:00
Koen J
e0b5e7b808 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-06 16:55:51 +02:00
Koen J
ac3a8da002 Various fixes for android to android pairing. 2025-05-06 16:54:58 +02:00
Kelvin
1aa45c2156 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-06 13:23:10 +02:00
Kelvin
3cf8abd409 Fix racecondition watchlater adds 2025-05-06 13:23:00 +02:00
Koen J
db8426779c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-06 13:04:53 +02:00
Koen J
b419e033f3 Casting device more clearly communicates when not ready. Implemented backoffs for SyncServer. 2025-05-06 13:04:42 +02:00
Kelvin
d686fa327b Incorrect gzip compression 2025-05-06 12:40:24 +02:00
Kelvin
a1ce5eda43 Fix synced ImageVariables showing black images 2025-05-06 11:53:30 +02:00
Koen J
1e790d1aa9 Added toggle to be able to disable local functionality for sync. Sync now automatically closes when pairing is successful. Pairing in progress layouts now properly show again. 2025-05-05 13:34:52 +02:00
Koen J
d1d304b758 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-05 12:00:15 +02:00
Koen J
e12b500144 Sync disabled by default. 2025-05-05 12:00:04 +02:00
Kelvin
bd77651a1e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-05 11:18:57 +02:00
Kelvin
35dc186395 Login edgecase fix 2025-05-05 11:18:46 +02:00
Koen J
07e78e0d12 Fixed sending packets without data in sync protocol. 2025-05-05 10:41:21 +02:00
Koen J
5b8905c1d2 Made ipv6 casting URL fix. 2025-05-04 00:50:12 +02:00
Koen J
158a27cbae Casting fixes. 2025-05-03 21:20:19 +02:00
Koen J
5769b39d78 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-03 21:05:23 +02:00
Koen J
5c96262c75 Crashfix. 2025-05-03 21:05:12 +02:00
Koen J
766f57dc9d Crashfix. 2025-05-03 21:04:39 +02:00
Kelvin
9986078582 Fix sync crash and responsiveness for subs sync 2025-05-03 19:30:47 +02:00
Koen J
e047ab5684 Crashfixes. 2025-05-03 17:35:17 +02:00
Koen J
a100785ad7 Gzip only for data packets. 2025-05-03 17:09:34 +02:00
Koen J
156eb4d15e Implemented sync protocol gzip. 2025-05-03 15:00:17 +02:00
Koen J
dabcfd965f Fixed send on wrong thread. 2025-05-03 12:11:08 +02:00
Koen J
d44a71f3be Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-03 11:22:06 +02:00
Koen J
f8edd6cf3d Possibel performance improvements to sync under high lat conditions. 2025-05-03 11:21:57 +02:00
Kelvin
2baf53c5a4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-02 12:10:58 +02:00
Kelvin
c26e9c281f Easier search type switching on results page, fix search result click channels 2025-05-02 12:10:47 +02:00
Koen J
9f78e9b7dd Crashfix in nsdmanager. StateSync reconnects less often. Channels are closed when sending fails in sync. 2025-05-01 21:53:48 +02:00
Kelvin
fdaf41b605 BuildPlatform property 2025-05-01 16:31:51 +02:00
Koen J
89526efe7a Updated submodules. 2025-05-01 14:11:22 +02:00
Koen J
5e3a25c18f Added dialog with loader before QR code scanner shows. 2025-05-01 11:00:33 +02:00
Koen J
cf11c4283e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-01 09:56:25 +02:00
Koen J
2dde04b979 Reduced padding on content types search. 2025-05-01 09:55:33 +02:00
Kelvin
8384f227be Plugin refs 2025-04-30 20:02:09 +02:00
Kelvin
697b3bc5f5 SLD domain checking fix, download notification if on metered, check for unstarted downloads on opening ui, minor fixes/imrpovements 2025-04-30 20:00:48 +02:00
Koen J
9e2041521e Made the disconnect button easier to click on casting connected dialog. 2025-04-29 15:27:24 +02:00
Koen J
ee7b89ec6e Added new casting dialog. 2025-04-29 15:22:06 +02:00
Koen J
5b143bdc76 Switch to NsdManager. 2025-04-29 08:39:05 +02:00
Kai
e3800426c9
Fix https://github.com/futo-org/grayjay-android/issues/1721
By setting video information section to visibility gone when going fullscreen

Changelog: changed
2025-04-28 10:13:26 -05:00
Koen J
d9d00e452e Explicitly set network interface in joinGroup. 2025-04-28 16:59:11 +02:00
Koen J
14500e281c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-04-28 13:59:59 +02:00
Koen J
c4623c80ff Implemented app id and updated unit tests. 2025-04-28 13:59:50 +02:00
Kelvin
9e17dce9a9 Fix edgecase where activity killed before 5s after opening 2025-04-25 17:40:45 +02:00
Kai
4acc867634
make channel shorts tab work correctly for linked channels
Changelog: changed
2025-04-23 11:38:09 -05:00
Kai
1a061268de
restore brightness when leaving app while a video is full screen
Changelog: changed
2025-04-23 09:30:07 -05:00
Koen J
daa91986ef Add search type selector to suggestions fragment. 2025-04-22 13:40:05 +02:00
Koen J
63761cfc9a Simplified all searches to use ContentSearchResultsFragment. 2025-04-22 13:08:23 +02:00
Kai
5091a5485a
grayjay only supports dark theme. set that on launch
Changelog: changed
2025-04-21 12:37:12 -05:00
Koen J
d10026acd1 Added ping loop. 2025-04-21 13:32:58 +02:00
Kai
f8f1cababe
increase analyze duration
Changelog: changed
2025-04-17 15:06:30 -05:00
Chris Olin
ad46841397
Update AndroidManifest.xml
resolves https://github.com/futo-org/grayjay-android/issues/47
2025-04-16 11:17:39 -04:00
Tim B
20fb1e0fd0 Added monochrome launcher icon 2025-04-16 02:44:40 +02:00
Koen J
9347351c37 Fixed issue where it would continuously try to connect over relay. 2025-04-15 09:39:35 +02:00
Kai
42dd8d6152
add TODO
Changelog: changed
2025-04-14 12:40:47 -05:00
Koen J
0ef1f2d40f Added LinkType to Channel. 2025-04-14 15:19:16 +02:00
Koen J
b460f9915d Added settings for enabling/disabling remote sync features. Fixed device pairing success showing too early. 2025-04-14 14:41:47 +02:00
Koen J
4e195dfbc3 Rename to direct and relayed. 2025-04-14 10:38:42 +02:00
buzzcola3
38b9fe3017 Fix prevQueueItem always returning the item from _queue, even when _queueShuffled is active 2025-04-13 20:36:15 +02:00
Kai
0a839b4814
fix crash
Changelog: changed
2025-04-11 10:43:33 -05:00
Koen
3c7f7bfca7 Merge branch 'remote-sync' into 'master'
Implemented remote sync.

See merge request videostreaming/grayjay!93
2025-04-11 14:31:47 +00:00
Koen
05230971b3 Implemented remote sync. 2025-04-11 14:31:47 +00:00
Kai
586db317dd
catch exception remove UI lag
Changelog: changed
2025-04-10 10:17:10 -05:00
Kai
ae36a24ad1
catch exception remove UI lag
Changelog: changed
2025-04-10 10:16:30 -05:00
Kelvin K
dccdf72c73 Message change 2025-04-09 23:35:44 +02:00
Kelvin K
ca15983a72 Casting message, caching creator images 2025-04-09 23:26:35 +02:00
Kelvin K
4b6a2c9829 Keyboard hide on search end 2025-04-09 21:02:19 +02:00
Kai
9a435f8859
fix loading bar and app switching
Changelog: changed
2025-04-09 12:16:07 -05:00
Kai
81162c5df2
fix crash on early loading
Changelog: changed
2025-04-08 18:58:56 -05:00
Kai
c7c3ddfc96
fix progress bar offset
Changelog: changed
2025-04-08 18:43:40 -05:00
Kai
830d3a9022
Merge branch 'master' into shorts-tab 2025-04-08 17:58:55 -05:00
Kai
a1c2d19daf
refactor shorts code
Changelog: changed
2025-04-08 17:58:31 -05:00
Kelvin K
1755d03a6b Fcast clearer connection/reconnection overlay, disable ipv6 by default 2025-04-09 00:56:49 +02:00
Kelvin K
869b1fc15e Fix pager for landscape 2025-04-08 00:34:52 +02:00
Kelvin K
ce2a2f8582 submods 2025-04-07 23:32:57 +02:00
Kelvin K
7b355139fb Subscription persistence fixes, home toggle fixes, subs exchange gzip, etc 2025-04-07 23:31:00 +02:00
Kelvin K
b14518edb1 Home filter fixes, persistent sorts, subs exchange fixes, playlist video options 2025-04-05 01:02:50 +02:00
Kelvin K
7d64003d1c Feed filter loading improved, home filters support, various peripheral stuff 2025-04-04 00:37:26 +02:00
Kelvin K
0a59e04f19 Fix ui offset issue when opening video through search url 2025-04-02 23:40:37 +02:00
Kelvin
b57abb646f Merge branch 'subs-exchange' into 'master'
Experimental Subs Exchange

See merge request videostreaming/grayjay!91
2025-04-02 21:12:07 +00:00
Kelvin K
dd6bde97a9 Playlists sort and search support, Playlist search support, wip local playback, other fixes 2025-04-02 22:53:54 +02:00
Kai
bd87a47551
finished UI and interactions
Changelog: added
2025-04-01 11:25:07 -05:00
Kelvin K
b545545712 Remove dep 2025-04-01 01:01:45 +02:00
Kelvin K
c1993ffa03 SubsExchange fixes 2025-04-01 00:56:24 +02:00
Kelvin K
7f7ebafa46 Resume on playback error instead of reseting, dont error on empty author url, subs exchange fixes 2025-03-31 20:02:49 +02:00
Kelvin K
b652597924 chapters ui on text press 2025-03-28 21:40:17 +01:00
Koen
258fe77928 Merge branch 'add-plugin-tedtalks' into 'master'
add ted talks plugin

See merge request videostreaming/grayjay!90
2025-03-28 19:42:30 +00:00
Stefan
5a9fcd6fab add ted talks plugin 2025-03-28 19:13:55 +00:00
Kelvin K
3c05521a5b Chapter Overlay 2025-03-27 23:25:13 +01:00
Kelvin K
034b8b15ae WIP SubsExchange 2025-03-26 23:28:32 +01:00
Kelvin K
7bd687331b AddSource by url support, submods 2025-03-24 20:33:17 +01:00
Kai DeLorenzo
bdae35b1a8 Merge branch 'master' into 'small-hls-fixes'
# Conflicts:
#   app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
2025-03-24 02:12:27 +00:00
Kelvin K
54d58df4b6 Sync watch later on initial connection, Original audio boolean support, priority audio support, setting to prefer original audio 2025-03-21 02:23:55 +01:00
Kai DeLorenzo
76103a2a8c
fix dialog missing 2025-03-19 15:12:48 -05:00
Kai DeLorenzo
9165a9f7cb Add : support for login button selector 2025-03-17 23:24:15 +00:00
Kelvin
b556d1e81d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-03-10 22:12:27 +01:00
Kelvin
7c25678211 Subgroup sub image url, ImageVariable default error icon on fail to load 2025-03-10 22:12:17 +01:00
Kai
f63f9dd6db
initial POC shorts tab
Changelog: added
2025-03-07 14:27:18 -06:00
Koen J
c83a9924e2 Implemented new ApiMethods calls. 2025-03-05 17:04:48 +01:00
Koen J
bbeb9b83a0 Removed dynamic Polycentric calls. 2025-03-05 11:58:09 +01:00
Kelvin
06478f3e36 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-27 14:34:59 +01:00
Kelvin
40f20002b2 submods 2025-02-27 14:34:51 +01:00
Koen J
442272f517 SettingsActivity can now be landscape. 2025-02-27 10:38:03 +01:00
Kelvin
88dae8e9c4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-26 21:29:40 +01:00
Kelvin
1bbfa7d39e WIP home filtering 2025-02-26 21:29:06 +01:00
Koen J
edc2b3d295 Fixed issue where video reload would reset video timestamp. 2025-02-25 15:32:30 +01:00
Koen J
0006da7385 Implemented sync display names. 2025-02-25 11:00:54 +01:00
Kai
470b7bd2e5
add non iv version
Changelog: added
2025-02-20 21:27:44 -06:00
Kai
9014fb581d
add support for downloading encrypted HLS streams
Changelog: changed
2025-02-20 16:07:35 -06:00
Kai DeLorenzo
b5ac8b3ec6 Edit Authentication.md 2025-02-20 21:59:29 +00:00
Kai
78f5169880
add recommendations assignment in video details class
Changelog: added
2025-02-20 14:43:13 -06:00
Kai
7ffa6b1bb3
revert params
Changelog: changed
2025-02-19 12:32:17 -06:00
Kai
3cd4b4503f
fix other cookie handling
Changelog: changed
2025-02-19 11:59:59 -06:00
Kelvin
3361b77aec Remove accidental always update 2025-02-13 21:00:02 +01:00
Kelvin
8b7c9df286 Add to queue button on recommendations, no toast on add to watch later if dup 2025-02-12 20:16:50 +01:00
Kelvin
157d5b4c36 Fix container id conflict 2025-02-12 20:03:33 +01:00
Kelvin
44c8800bec plugin disabled update check fix 2025-02-12 19:25:29 +01:00
Kelvin
2f0ba1b1f7 Setting to check disabled plugins for updates (off by default) 2025-02-12 19:17:20 +01:00
Kelvin
36c51f1a0c Refs 2025-02-12 19:06:43 +01:00
Kelvin
1dfe18aa6f Add Apple podcasts 2025-02-12 18:58:01 +01:00
Kelvin
b9bbfb44c5 Update submodules, fix apple podcast dir 2025-02-12 18:53:30 +01:00
Kelvin
83843f192d Show total downloaded content duration, Indicator how many subscriptions, save queue as playlist 2025-02-12 18:43:15 +01:00
Kelvin
8839d9f1c6 Fix for misisng exports for export playlist 2025-02-12 16:31:30 +01:00
Kelvin
0630ec1d46 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-11 20:31:33 +01:00
Kelvin
4dce8d6a80 Export playlist support 2025-02-11 20:31:26 +01:00
Koen J
3b62f999bf Fixed HttpFileHandler bug causing casting local webm not to work. 2025-02-11 17:41:25 +01:00
Kai
d63fa521a1
Merge remote-tracking branch 'origin/hls-audio-fixes' into hls-audio-fixes
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
2025-02-11 10:36:56 -06:00
Kai
ca781dfe15
restore hard code HLS as mp4
Changelog: changed
2025-02-11 10:35:05 -06:00
Kai
4bc561ceab
fix HLS audio download and download audio only
Changelog: changed
2025-02-11 10:34:39 -06:00
Kai
3d258180bd
restore hard code HLS as mp4
Changelog: changed
2025-02-11 10:31:47 -06:00
Kelvin
65ae8610fd Hide download for live videos 2025-02-11 17:06:57 +01:00
Kelvin
c1c2000c98 Download container fixes 2025-02-11 16:13:07 +01:00
Kelvin
287c2d82a1 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-11 14:13:55 +01:00
Kelvin
5cde1650f4 DashRaw streammetadata fetching 2025-02-11 14:13:48 +01:00
Kelvin
a4b90f14ab Merge branch 'hls-audio-only-download' into 'master'
Hide audio only option when no audio sources

See merge request videostreaming/grayjay!87
2025-02-11 12:30:53 +00:00
Koen J
4826b40136 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-11 10:32:24 +01:00
Koen J
62618224da Casting server did not bind to an automatically selected port. 2025-02-11 10:32:11 +01:00
Kai
49f15e1637
don't show audio only download option if there aren't any audio sources available
for HLS and DASH the HLS and DASH pickers give the option to only download audio

Changelog: changed
2025-02-10 22:32:56 -06:00
Kai
d5cab0910e
fix HLS audio download and download audio only
Changelog: changed
2025-02-10 22:21:06 -06:00
Kai
d4ccf232c1
fix HLS download odysee nebula peertube
Changelog: changed
2025-02-10 17:57:27 -06:00
Kelvin
e36047c890 Merge branch 'prevent-exception-replay' into 'master'
Prevent Exception Replay When Unsubscribing From Deleted Channel

See merge request videostreaming/grayjay!77
2025-02-10 19:15:09 +00:00
Kelvin
8f1199bd08 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-10 20:12:50 +01:00
Kelvin
d6e045ea4e JSDOM, optional packages, Channel not crash if opened without plugin, downloads ordering fixes/naming 2025-02-10 20:12:43 +01:00
Kelvin
304e48996b Merge branch 'rotation-lock-fix' into 'master'
Fix Rotation Lock Activity Resume Issue

See merge request videostreaming/grayjay!83
2025-02-10 18:55:19 +00:00
Kelvin
f350dc83b8 Merge branch 'brightness-fix' into 'master'
Restore Overlay Brightness When Re-entering Full Screen

See merge request videostreaming/grayjay!84
2025-02-10 18:53:38 +00:00
Kelvin
ebb7beda8c Merge branch 'fix-background-playback' into 'master'
Background playback support for HLS and DASH

See merge request videostreaming/grayjay!80
2025-02-10 18:48:05 +00:00
Kelvin
a01f3da66e Merge branch 'adaptive-streaming-auto-ui' into 'master'
Auto mode UI for adaptive streams (HLS and DASH)

See merge request videostreaming/grayjay!76
2025-02-10 18:47:50 +00:00
Kelvin
72f5b5fbc0 Ref 2025-02-07 19:08:38 +01:00
Kelvin
330aa495c8 Playlist dup prevention, download search and ordering, optional package support 2025-02-06 21:36:33 +01:00
Kelvin
0b529ae94d Plugin changelog support, Hide hidden from search setting, No author change warning if missing pubkey, toast on add to playlist, better autoplay description, Playlist total duration label 2025-02-06 19:19:29 +01:00
Kelvin
83b35183d0 Channel shorts tab, Forced batch parallelization, Playlist download support for live sources, hardware codec query 2025-02-05 19:40:28 +01:00
Alexandre Picavet
daf1d42a0f
feat(player): Add a setting to adjust player seek duration
Create a seekOffset dropdown setting defaulting to 10 seconds.
Update the fastForwardTick method  of the GestureControlView to take the
seekOffset setting into account and update the view accordingly.
2025-02-04 22:00:08 +01:00
Kelvin
2cd01eb1fe Merge 2025-02-03 21:38:18 +01:00
Kelvin
07378f665a Fix http memory leak for byte responses, refs 2025-02-03 21:36:41 +01:00
Kai DeLorenzo
bfd5f24f4c Fix https://github.com/futo-org/grayjay-android/issues/727 2025-01-27 21:51:17 +00:00
Kai
3d617187af
update rotation lock approach
Changelog: changed
2025-01-27 12:03:25 -06:00
Koen J
d040b93ca9 Updated submodules and fixed IPv6 casting play address being wrong. 2025-01-23 14:26:08 +01:00
Kai
a1d460385d
prevent the user from needing to tap update on system dialog when self updating
Changelog: added
2025-01-20 15:38:26 -06:00
Koen J
a410e2962a Only take one line for signing. 2025-01-20 14:04:55 +01:00
Koen J
f5aa8f37bb Updated youtube. 2025-01-17 22:19:02 +01:00
Koen J
7e932df450 Updated submodules. 2025-01-17 22:01:26 +01:00
Koen J
3d4741727e Updated submodules. 2025-01-17 21:37:28 +01:00
Kelvin
a03b63ef74 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-01-17 21:27:23 +01:00
Kelvin
15ce3e9f20 Timeout support 2025-01-17 21:27:12 +01:00
ajp-dev
d2ed0c65ca
fix link typo for Script Signing
typo causes HTTP 400 error when accessing link to Script Signing.md
2025-01-17 17:05:49 +08:00
Kai
da58b72f9d
add background playback support for videos without an explicit audio source
Changelog: changed
2025-01-14 11:36:37 -06:00
Kai DeLorenzo
1639bd7af1 Merge branch 'improve-full-screen-portrait-docs' into 'master'
improve full screen portrait docs

See merge request videostreaming/grayjay!79
2025-01-14 16:00:51 +00:00
Kai
d474121f85
improve full screen portrait docs
Changelog: changed
2025-01-14 09:59:17 -06:00
Kai
978f76ffb6
Added current quality to auto item
Changelog: added
2025-01-10 14:38:54 -06:00
Kai
084bac00f5
Clear feed loading exceptions to prevent replay of exceptions
Changelog: changed
2025-01-08 21:54:03 -06:00
Kai
94454172dd
Add UI to show when adaptive streams (HLS and DASH) are in auto mode
Changelog: added
2025-01-08 20:43:38 -06:00
Koen J
891d3cf966 Added debug message. 2025-01-06 16:55:02 +01:00
Koen J
561d5ec7ab Fixes to sync. 2025-01-06 15:56:31 +01:00
Koen J
7ce437d50a Updated submodule. 2025-01-06 11:42:14 +01:00
Kelvin
4b02d4ce90 Merge branch 'zvonimir-dev' into 'master'
workflow: Remove labeler

See merge request videostreaming/grayjay!74
2024-12-23 20:44:25 +00:00
Zvonimir Zrakić
3107185869 workflow: Remove labeler 2024-12-23 21:31:06 +01:00
Koen
2e3584a353 Merge branch 'zvonimir-dev' into 'master'
Improve issue templates

See merge request videostreaming/grayjay!73
2024-12-23 18:36:32 +00:00
Zvonimir Zrakić
e5b1be195c fix: Fix issue templates, add new plugins 2024-12-23 18:45:35 +01:00
Zvonimir Zrakić
dde30c9d76 Add VPN option and fix label typo 2024-12-23 18:45:18 +01:00
Koen
3830e65de8 Update bug_report.yml 2024-12-23 15:43:40 +00:00
Koen
c589cf167e Merge branch 'zvonimir-dev' into 'master'
Update issue templates

See merge request videostreaming/grayjay!72
2024-12-21 22:05:42 +00:00
Zvonimir Zrakić
2fde367c82 Update issue templates 2024-12-21 22:05:42 +00:00
Kai DeLorenzo
8fd188268e Merge branch 'disable-download' into 'master'
disable download for Widevine sources

See merge request videostreaming/grayjay!70
2024-12-21 17:27:15 +00:00
Kai
b65257df42
disable download for Widevine sources 2024-12-21 11:26:10 -06:00
Kelvin
aaa2d7f08d Prevent crashes on non-existing assets 2024-12-19 22:00:56 +01:00
Kelvin
f73e25ece6 Fix crash activity update support 2024-12-19 21:54:32 +01:00
Kelvin
78d427f208 Remove broken ref for now 2024-12-19 21:14:07 +01:00
Kelvin
eaeaf3538f Better messaging on failed to connect sync 2024-12-18 22:20:11 +01:00
Kai DeLorenzo
85e381a85e Merge branch 'update-deps' into 'master'
update deps

See merge request videostreaming/grayjay!69
2024-12-18 20:37:07 +00:00
Kai
1b7ee8231b
update deps 2024-12-18 14:36:40 -06:00
Kai DeLorenzo
1b8b8f5738 Merge branch 'tablet-rotation-issue' into 'master'
more recent landscape and rotation issues

See merge request videostreaming/grayjay!66
2024-12-14 20:42:11 +00:00
Kai
53df19b477
fixes for:
weird tablet issues on some screen sizes
horizontal maximized player on phones
always allow full screen rotation not working
2024-12-14 14:41:28 -06:00
Kai DeLorenzo
ccf21b7580 Merge branch 'rotation-regression' into 'master'
fix rotation regression

See merge request videostreaming/grayjay!65
2024-12-14 03:13:21 +00:00
Kai
4189d62a57
fix rotation regression 2024-12-13 20:47:21 -06:00
Koen
9a3e3af614 Merge branch 'feat/apple-podcasts-plugin' into 'master'
Add Apple Podcasts plugin

See merge request videostreaming/grayjay!63
2024-12-13 18:55:16 +00:00
Stefan
f7187400dc Add Apple Podcasts plugin 2024-12-13 17:59:56 +00:00
Kai DeLorenzo
f55a7f0a7b Merge branch 'detect-system-setting-changes' into 'master'
detect system auto rotate setting changes

See merge request videostreaming/grayjay!62
2024-12-13 17:46:00 +00:00
Kai
d6d35a645e
detect system auto rotate setting changes
correctly handle lock button when full screen
2024-12-13 11:44:57 -06:00
Kai
e719dcc7f5
detect system auto rotate setting changes
correctly handle lock button when full screen
2024-12-13 11:23:40 -06:00
Kai DeLorenzo
bc5bc5450c Merge branch 'change-default-auto-rotate-setting' into 'master'
change default to force auto rotate while full screen

See merge request videostreaming/grayjay!61
2024-12-13 16:22:00 +00:00
Kai
f4bade0c2e
change default to force auto rotate while full screen 2024-12-13 10:21:40 -06:00
Koen J
9be59c674d Updated YouTube. 2024-12-13 17:13:42 +01:00
Kai DeLorenzo
a1dec23c20 Merge branch 'default-reset-auto-rotate-disabled' into 'master'
remove assumptions about rotation preference

See merge request videostreaming/grayjay!60
2024-12-13 16:01:07 +00:00
Kai
ed926c4e37
remove assumptions about rotation preference 2024-12-13 10:00:43 -06:00
Kai DeLorenzo
ab360ed6f6 Merge branch 'force-leave-landscape-tweak' into 'master'
Force leave landscape tweak

See merge request videostreaming/grayjay!59
2024-12-13 15:43:33 +00:00
Kai
569ba3d651
suppress warning 2024-12-13 09:43:08 -06:00
Kai
60fe28c2fe
simplified rotation logic 2024-12-13 09:41:52 -06:00
Kai DeLorenzo
2787e29a07 Merge branch 'delay-tweak' into 'master'
increase delay to prevent erroneous rotations

See merge request videostreaming/grayjay!58
2024-12-13 05:02:23 +00:00
Kai
c77a4d08d6
increase delay to prevent erroneous rotations 2024-12-12 23:01:28 -06:00
Kai DeLorenzo
9b3f90f922 Merge branch 'auto-rotate-fixes' into 'master'
more rotation/orientation fixes

See merge request videostreaming/grayjay!57
2024-12-12 21:27:20 +00:00
Kai
c88d457021
fix device getting stuck landscape or in portrait when entering and exiting full screen via the button
fix weird rotation behavior when locking and unlocking rotation via the player button
2024-12-12 15:25:02 -06:00
Koen J
b20b625820 Fixed compiler errors. 2024-12-12 16:02:37 +01:00
Koen J
fd95311920 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-12 14:13:34 +01:00
Koen J
6da5c11731 Fixed concurrent modification crash in ServiceRecordAggregator and link clicking in scroll. 2024-12-12 14:13:24 +01:00
Koen
4e58231308 Merge branch 'revamp-rotation-settings' into 'master'
Update and simplify rotation settings

See merge request videostreaming/grayjay!56
2024-12-12 12:56:21 +00:00
Kai
ef0ecf249a
update rotation settings 2024-12-11 14:38:58 -06:00
Kelvin
4981617f7a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-11 20:24:17 +01:00
Kelvin
2070bc7007 Refs 2024-12-11 20:24:09 +01:00
Kai DeLorenzo
231d2461b3 Merge branch 'incorrect-number-of-columns-bug' into 'master'
fix the calculation that incorrectly sets the number of columns to display

See merge request videostreaming/grayjay!54
2024-12-11 17:48:17 +00:00
Kai DeLorenzo
3b457f87c4 Merge branch 'fix-fullscreen-from-pip' into 'master'
prevent going into full screen when entering pip mode

See merge request videostreaming/grayjay!55
2024-12-11 17:48:00 +00:00
Koen J
de3ced4d3c Intent class should be MediaButtonReceiver. 2024-12-11 10:41:36 +01:00
Koen J
891777e89e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-11 10:36:13 +01:00
Koen J
287239dd1c Added media button receiver. 2024-12-11 10:36:02 +01:00
Kelvin
7cdded8fd7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-10 22:35:46 +01:00
Kelvin
8c9d045e1d Download playlist fix for videos without audio file 2024-12-10 22:35:38 +01:00
Kai
620f5a0459
prevent going into full screen when entering pip mode 2024-12-10 11:30:48 -06:00
Koen
178d874ba0 Merge branch 'spinner-block-player' into 'master'
Spinner block player go full screen

See merge request videostreaming/grayjay!53
2024-12-10 16:18:55 +00:00
Koen
d44f30c8a6 Merge branch 'creator-filter-clear' into 'master'
enable creator filter clear

See merge request videostreaming/grayjay!52
2024-12-10 15:56:42 +00:00
Koen J
ce66937429 Offline playback toast now doesn't show more than once every 5 seconds. 2024-12-10 14:01:34 +01:00
Koen J
9823337375 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-10 13:54:29 +01:00
Koen J
11f5f0dfe1 Fixed comment character counter. 2024-12-10 13:54:02 +01:00
Koen J
e1882f19e8 Comment close now requires confirmation. Fixed comment character counter. 2024-12-10 13:52:24 +01:00
Koen J
6a8b9f06c2 Comment close now requires confirmation. 2024-12-10 13:52:02 +01:00
Koen J
752fc8787d Fixed link scrolling behaviour. 2024-12-10 13:29:09 +01:00
Koen J
90a1cd8280 Placing reply comments works again. 2024-12-10 13:07:03 +01:00
Koen J
aa570ac29d Updated submodules. 2024-12-10 12:47:18 +01:00
Kai
fb7b6363f9
added multi column support for channels 2024-12-09 18:00:24 -06:00
Kai
23afe7994c
fix the calculation that incorrectly sets the number of columns to display 2024-12-08 16:47:13 -06:00
Kai
7557e6f6ba
prevent going full screen before the video has loaded 2024-12-07 11:32:28 -06:00
Kai
86b6938911
added video detail check 2024-12-07 11:09:48 -06:00
Kai
8f30a45fa8
enable creator filter clear 2024-12-06 11:13:29 -06:00
Koen J
7c9e9d5f52 Should not crash app when StateSync fails to bind. 2024-12-06 17:49:18 +01:00
Koen J
4066ce73a8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-06 14:55:10 +01:00
Koen J
b5722dba1a MainActivity should be singleInstance. 2024-12-06 14:54:57 +01:00
Koen
81765ecafc Update deploy-playstore.sh 2024-12-06 10:54:36 +00:00
Kelvin
84b42e9d19 refs 2024-12-05 17:08:30 +01:00
Koen J
ed319a0e5f Fixed invalid padding. 2024-12-05 17:03:45 +01:00
Koen J
dd55d10194 Crashfix. 2024-12-05 17:00:23 +01:00
Koen J
2084b46090 Possible fix for sub bar distance. 2024-12-05 16:58:35 +01:00
Koen J
53443a6cf2 Only show announcements on subscriptions if home is not enabled. Add margin top to subscriptions. 2024-12-05 16:51:56 +01:00
Koen J
92715b5642 Fixed crash due to AnnouncementView. 2024-12-05 16:40:05 +01:00
479 changed files with 20286 additions and 7026 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
aar/* filter=lfs diff=lfs merge=lfs -text
app/aar/* filter=lfs diff=lfs merge=lfs -text

166
.github/ISSUE_TEMPLATE/1-bug_report.yml vendored Normal file
View file

@ -0,0 +1,166 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["Bug", "Android"]
title: "Bug: "
type: bug
projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for taking the time to fill out this bug report.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
id: reproduction-steps
attributes:
label: Reproduction steps
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
placeholder: |
0. Play a Youtube video
1. Press on Download button
2. Select quality 1440p
3. Grayjay crashes when attempting to download
validations:
required: true
- type: textarea
id: actual-result
attributes:
label: Actual result
description: What happend?
placeholder: Tell us what you saw!
validations:
required: true
- type: textarea
id: expected-result
attributes:
label: Expected result
description: What was suppose to happen?
placeholder: Tell us what you expected to happen!
validations:
required: true
- type: input
id: grayjay-version
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "311"
validations:
required: true
- type: dropdown
id: plugin
attributes:
label: What plugins are you seeing the problem on?
multiple: true
options:
- "All"
- "Apple Podcasts"
- "BiliBili (CN)"
- "Bitchute"
- "Crunchyroll"
- "CuriosityStream"
- "Dailymotion"
- "Kick"
- "Nebula"
- "Odysee"
- "Patreon"
- "PeerTube"
- "Rumble"
- "SoundCloud"
- "Spotify"
- "TedTalks"
- "Twitch"
- "Youtube"
- "Other"
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
- type: input
id: android-version
attributes:
label: Which android version are you using?
placeholder: "Android 15"
validations:
required: true
- type: input
id: phone-model
attributes:
label: Which device are you using?
placeholder: "Google Pixel 9"
validations:
required: true
- type: input
id: os-version
attributes:
label: Which operating system are you using?
placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..."
validations:
required: true
- type: checkboxes
id: login
attributes:
label: When do you experience the issue?
options:
- label: While logged in
- label: While logged out
- label: N/A
- type: dropdown
id: vpn
attributes:
label: Are you using a VPN?
multiple: false
options:
- "No"
- "Yes"
validations:
required: true
- type: textarea
id: grayjay-references
attributes:
label: References
description: |
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above? If so, please create a list below that mentions each of them. For example:
```
- #10
```
placeholder:
value:
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: markdown
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.

View file

@ -1,15 +1,16 @@
name: Feature Request
description: Suggest a new feature or other enhancement.
labels: ["enhancement", "new"]
labels: ["Enhancement", "Android"]
title: "Feature request: "
type: feature
projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a feature request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
[External Contributions are closed at this time](https://github.com/tom-futo/grayjay-android/blob/master/CONTRIBUTION.md#contributing-to-core)
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
@ -55,4 +56,4 @@ body:
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.

View file

@ -1,13 +1,16 @@
name: Documentation Issue
description: Report an issue or suggest a change in the documentation.
labels: ["documentation", "new"]
labels: ["Documentation"]
title: "Documentation: "
type: task
projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a documentation change request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)

View file

@ -1,80 +0,0 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["bug", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for taking the time to fill out this bug report.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you expect to happen?
placeholder: Tell us what you see!
validations:
required: true
- type: input
id: grayjay-version
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "242"
validations:
required: true
- type: dropdown
id: plugin
attributes:
label: What plugins are you seeing the problem on?
multiple: true
options:
- All
- Youtube
- BiliBili (CN)
- Twitch
- Odysee
- Rumble
- Kick
- PeerTube
- Patreon
- Nebula
- SoundCloud
- Other
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
- type: checkboxes
id: login
attributes:
label: When do you experience the issue?
options:
- label: While logged in
- label: While logged out
- label: N/A
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View file

@ -1,34 +0,0 @@
name: Issue labeler
on:
issues:
types: [ opened ]
permissions:
contents: read
jobs:
label-component:
runs-on: ubuntu-latest
permissions:
# required for all workflows
issues: write
steps:
- uses: actions/checkout@v3
- name: Parse issue form
uses: stefanbuck/github-issue-parser@v3
id: issue-parser
with:
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
- name: Set labels based on plugin field
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
with:
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
section: plugin
block-list: |
None
Other
token: ${{ secrets.GITHUB_TOKEN }}

24
.gitmodules vendored
View file

@ -82,3 +82,27 @@
[submodule "app/src/stable/assets/sources/dailymotion"]
path = app/src/stable/assets/sources/dailymotion
url = ../plugins/dailymotion.git
[submodule "app/src/stable/assets/sources/apple-podcast"]
path = app/src/stable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
path = app/src/unstable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git
[submodule "app/src/stable/assets/sources/tedtalks"]
path = app/src/stable/assets/sources/tedtalks
url = ../plugins/tedtalks.git
[submodule "app/src/unstable/assets/sources/tedtalks"]
path = app/src/unstable/assets/sources/tedtalks
url = ../plugins/tedtalks.git
[submodule "app/src/stable/assets/sources/curiositystream"]
path = app/src/stable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/curiositystream"]
path = app/src/unstable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/crunchyroll"]
path = app/src/unstable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git
[submodule "app/src/stable/assets/sources/crunchyroll"]
path = app/src/stable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git

BIN
app/aar/ffmpeg-kit-full-6.0-2.LTS.aar (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -156,6 +156,7 @@ android {
dependencies {
implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
implementation 'com.google.android.material:material:1.12.0'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core
@ -180,6 +181,7 @@ dependencies {
//JS
implementation("com.caoccao.javet:javet-android:3.0.2")
//implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
//Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1'
@ -197,7 +199,8 @@ dependencies {
implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
implementation fileTree(dir: 'aar', include: ['*.aar'])
implementation 'com.arthenica:smart-exception-java:0.2.1'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1'

View file

@ -0,0 +1,38 @@
package com.futo.platformplayer
import android.graphics.Color
import org.junit.Assert.assertEquals
import org.junit.Test
import toAndroidColor
class CSSColorTests {
@Test
fun test1() {
val androidHex = "#80336699"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#33669980"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor(),
)
}
@Test
fun test2() {
val androidHex = "#123ABC"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#123ABCFF"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor()
)
}
}

View file

@ -0,0 +1,338 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.select
import org.junit.Assert.*
import org.junit.Test
import java.net.Socket
import java.nio.ByteBuffer
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/*
class SyncServerTests {
//private val relayHost = "relay.grayjay.app"
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
private val relayHost = "192.168.1.138"
private val relayPort = 9000
/** Creates a client connected to the live relay server. */
private suspend fun createClient(
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
isHandshakeAllowed: ((LinkType, SyncSocketSession, String, String?, UInt) -> Boolean)? = null,
onException: ((Throwable) -> Unit)? = null
): SyncSocketSession = withContext(Dispatchers.IO) {
val p = Noise.createDH("25519")
p.generateKeyPair()
val socket = Socket(relayHost, relayPort)
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
val tcs = CompletableDeferred<Boolean>()
val socketSession = SyncSocketSession(
relayHost,
p,
inputStream,
outputStream,
onClose = { socket.close() },
onHandshakeComplete = { s ->
onHandshakeComplete?.invoke(s)
tcs.complete(true)
},
onData = onData ?: { _, _, _, _ -> },
onNewChannel = onNewChannel ?: { _, _ -> },
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _, _, _ -> true }
)
socketSession.authorizable = AlwaysAuthorized()
try {
socketSession.startAsInitiator(relayKey)
} catch (e: Throwable) {
onException?.invoke(e)
}
withTimeout(5000.milliseconds) { tcs.await() }
return@withContext socketSession
}
@Test
fun multipleClientsHandshake_Success() = runBlocking {
val client1 = createClient()
val client2 = createClient()
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
client1.stop()
client2.stop()
}
@Test
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
delay(100.milliseconds)
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
assertNotNull("Client B should receive connection info", infoB)
assertEquals(12345.toUShort(), infoB!!.port)
assertNull("Client C should not receive connection info (unauthorized)", infoC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun relayedTransport_Bidirectional_Success() = runBlocking {
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
val tcsDataA = CompletableDeferred<ByteArray>()
channelA.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
}
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
assertArrayEquals(maxSizeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
val data = byteArrayOf(1, 2, 3)
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
assertTrue(success)
assertNotNull(recordB)
assertArrayEquals(data, recordB!!.first)
assertNull("Unauthorized client should not access record", recordC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun getNonExistentRecord_ReturnsNull() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
assertNull("Getting non-existent record should return null", record)
clientA.stop()
clientB.stop()
}
@Test
fun updateRecord_TimestampUpdated() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val key = "updateKey"
val data1 = byteArrayOf(1)
val data2 = byteArrayOf(2)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
val record1 = clientB.getRecord(clientA.localPublicKey, key)
delay(1000.milliseconds)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
val record2 = clientB.getRecord(clientA.localPublicKey, key)
assertNotNull(record1)
assertNotNull(record2)
assertTrue(record2!!.second > record1!!.second)
assertArrayEquals(data2, record2.first)
clientA.stop()
clientB.stop()
}
@Test
fun deleteRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val data = byteArrayOf(1, 2, 3)
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
assertTrue(success)
assertNull(record)
clientA.stop()
clientB.stop()
}
@Test
fun listRecordKeys_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val keys = arrayOf("key1", "key2", "key3")
keys.forEach { key ->
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
}
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
clientA.stop()
clientB.stop()
}
@Test
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
assertArrayEquals(largeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetLargeRecord_Success() = runBlocking {
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
val clientA = createClient()
val clientB = createClient()
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
assertTrue(success)
assertNotNull(record)
assertArrayEquals(largeData, record!!.first)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithValidAppId_Success() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId }
)
val clientA = createClient()
// Act: Start relayed channel with valid appId
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey, appId = allowedAppId) }
val channelB = withTimeout(5.seconds) { tcsB.await() }
withTimeout(5.seconds) { channelTask.await() }
// Assert: Channel is established
assertNotNull("Channel should be created on target with valid appId", channelB)
// Clean up
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithInvalidAppId_Fails() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val invalidAppId = 5678u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId },
onException = { }
)
val clientA = createClient()
// Act & Assert: Attempt with invalid appId should fail
try {
withTimeout(5.seconds) {
clientA.startRelayedChannel(clientB.localPublicKey, appId = invalidAppId)
}
fail("Starting relayed channel with invalid appId should fail")
} catch (e: Throwable) {
// Expected: The channel creation should time out or fail
}
// Ensure no channel was created on client B
val completedTask = select {
tcsB.onAwait { "channel" }
async { delay(1.seconds); "timeout" }.onAwait { "timeout" }
}
assertEquals("No channel should be created with invalid appId", "timeout", completedTask)
// Clean up
clientA.stop()
clientB.stop()
}
}
class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true
}*/

View file

@ -0,0 +1,512 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import org.junit.Assert.*
import org.junit.Test
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.ByteBuffer
import kotlin.random.Random
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.Duration.Companion.seconds
/*
data class PipeStreams(
val initiatorInput: LittleEndianDataInputStream,
val initiatorOutput: LittleEndianDataOutputStream,
val responderInput: LittleEndianDataInputStream,
val responderOutput: LittleEndianDataOutputStream
)
typealias OnHandshakeComplete = (SyncSocketSession) -> Unit
typealias IsHandshakeAllowed = (LinkType, SyncSocketSession, String, String?, UInt) -> Boolean
typealias OnClose = (SyncSocketSession) -> Unit
typealias OnData = (SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit
class SyncSocketTests {
private fun createPipeStreams(): PipeStreams {
val initiatorOutput = PipedOutputStream()
val responderOutput = PipedOutputStream()
val responderInput = PipedInputStream(initiatorOutput)
val initiatorInput = PipedInputStream(responderOutput)
return PipeStreams(
LittleEndianDataInputStream(initiatorInput), LittleEndianDataOutputStream(initiatorOutput),
LittleEndianDataInputStream(responderInput), LittleEndianDataOutputStream(responderOutput)
)
}
fun generateKeyPair(): DHState {
val p = Noise.createDH("25519")
p.generateKeyPair()
return p
}
private fun createSessions(
initiatorInput: LittleEndianDataInputStream,
initiatorOutput: LittleEndianDataOutputStream,
responderInput: LittleEndianDataInputStream,
responderOutput: LittleEndianDataOutputStream,
initiatorKeyPair: DHState,
responderKeyPair: DHState,
onInitiatorHandshakeComplete: OnHandshakeComplete,
onResponderHandshakeComplete: OnHandshakeComplete,
onInitiatorClose: OnClose? = null,
onResponderClose: OnClose? = null,
onClose: OnClose? = null,
isHandshakeAllowed: IsHandshakeAllowed? = null,
onDataA: OnData? = null,
onDataB: OnData? = null
): Pair<SyncSocketSession, SyncSocketSession> {
val initiatorSession = SyncSocketSession(
"", initiatorKeyPair, initiatorInput, initiatorOutput,
onClose = {
onClose?.invoke(it)
onInitiatorClose?.invoke(it)
},
onHandshakeComplete = onInitiatorHandshakeComplete,
onData = onDataA,
isHandshakeAllowed = isHandshakeAllowed
)
val responderSession = SyncSocketSession(
"", responderKeyPair, responderInput, responderOutput,
onClose = {
onClose?.invoke(it)
onResponderClose?.invoke(it)
},
onHandshakeComplete = onResponderHandshakeComplete,
onData = onDataB,
isHandshakeAllowed = isHandshakeAllowed
)
return Pair(initiatorSession, responderSession)
}
@Test
fun handshake_WithValidPairingCode_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = validPairingCode)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun handshake_WithInvalidPairingCode_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val invalidPairingCode = "wrong"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = invalidPairingCode)
responderSession.startAsResponder()
withTimeout(100.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithoutPairingCodeWhenRequired_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey) // No pairing code
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithPairingCodeWhenNotRequired_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val pairingCode = "unnecessary"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, _, _ -> true } // Always allow
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = pairingCode)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun sendAndReceive_SmallDataPacket_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val smallData = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(smallData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(smallData, receivedData)
}
@Test
fun sendAndReceive_ExactlyMaximumPacketSize_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val maxData = ByteArray(SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(maxData, receivedData)
}
@Test
fun stream_LargeData_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val largeData = ByteArray(2 * (SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE)).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(largeData, receivedData)
}
@Test
fun authorizedSession_CanSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize both sessions
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(data, receivedData)
}
@Test
fun unauthorizedSession_CannotSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, _, _, _ -> }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize initiator but not responder
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Unauthorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
delay(1.seconds)
assertFalse(tcsDataReceived.isCompleted)
}
@Test
fun directHandshake_WithValidAppId_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = allowedAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
assertNotNull(initiatorSession.remotePublicKey)
assertNotNull(responderSession.remotePublicKey)
}
@Test
fun directHandshake_WithInvalidAppId_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val invalidAppId = 5678u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = invalidAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
}
class Authorized : IAuthorizable {
override val isAuthorized: Boolean = true
}
class Unauthorized : IAuthorizable {
override val isAuthorized: Boolean = false
}*/

View file

@ -36,6 +36,12 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider>
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service android:name=".services.MediaPlaybackService"
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
@ -49,10 +55,10 @@
<activity
android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleTask"
android:launchMode="singleInstance"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">
@ -150,7 +156,6 @@
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SettingsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.DeveloperActivity"
@ -234,4 +239,4 @@
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
</application>
</manifest>
</manifest>

File diff suppressed because one or more lines are too long

View file

@ -11,7 +11,8 @@ let Type = {
Streams: "STREAMS",
Mixed: "MIXED",
Live: "LIVE",
Subscriptions: "SUBSCRIPTIONS"
Subscriptions: "SUBSCRIPTIONS",
Shorts: "SHORTS"
},
Order: {
Chronological: "CHRONOLOGICAL"
@ -31,7 +32,8 @@ let Type = {
Text: {
RAW: 0,
HTML: 1,
MARKUP: 2
MARKUP: 2,
CODE: 3
},
Chapter: {
NORMAL: 0,
@ -101,6 +103,12 @@ class UnavailableException extends ScriptException {
super("UnavailableException", msg);
}
}
class ReloadRequiredException extends ScriptException {
constructor(msg, reloadData) {
super("ReloadRequiredException", msg);
this.reloadData = reloadData;
}
}
class AgeException extends ScriptException {
constructor(msg) {
super("AgeException", msg);
@ -243,7 +251,11 @@ class PlatformVideo extends PlatformContent {
this.duration = obj.duration ?? -1; //Long
this.viewCount = obj.viewCount ?? -1; //Long
this.playbackTime = obj.playbackTime ?? -1;
this.playbackDate = obj.playbackDate ?? undefined;
this.isLive = obj.isLive ?? false; //Boolean
this.isShort = !!obj.isShort ?? false;
}
}
class PlatformVideoDetails extends PlatformVideo {
@ -260,6 +272,11 @@ class PlatformVideoDetails extends PlatformVideo {
this.rating = obj.rating ?? null; //IRating
this.subtitles = obj.subtitles ?? [];
this.isShort = !!obj.isShort ?? false;
if (obj.getContentRecommendations) {
this.getContentRecommendations = obj.getContentRecommendations
}
}
}
@ -284,15 +301,39 @@ class PlatformPostDetails extends PlatformPost {
}
}
class PlatformArticleDetails extends PlatformContent {
class PlatformWeb extends PlatformContent {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWeb";
}
}
class PlatformWebDetails extends PlatformWeb {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWebDetails";
this.html = obj.html;
}
}
class PlatformArticle extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticle";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class PlatformArticleDetails extends PlatformArticle {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticleDetails";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.segments = obj.segments ?? [];
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class ArticleSegment {
@ -308,9 +349,17 @@ class ArticleTextSegment extends ArticleSegment {
}
}
class ArticleImagesSegment extends ArticleSegment {
constructor(images) {
constructor(images, caption) {
super(2);
this.images = images;
this.caption = caption;
}
}
class ArticleHeaderSegment extends ArticleSegment {
constructor(content, level) {
super(3);
this.level = level;
this.content = content;
}
}
class ArticleNestedSegment extends ArticleSegment {
@ -588,6 +637,8 @@ class PlatformComment {
this.date = obj.date ?? 0;
this.replyCount = obj.replyCount ?? 0;
this.context = obj.context ?? {};
if(obj.getReplies)
this.getReplies = obj.getReplies;
}
}
@ -659,11 +710,12 @@ class LiveEventViewCount extends LiveEvent {
}
}
class LiveEventRaid extends LiveEvent {
constructor(targetUrl, targetName, targetThumbnail) {
constructor(targetUrl, targetName, targetThumbnail, isOutgoing) {
super(100);
this.targetUrl = targetUrl;
this.targetName = targetName;
this.targetThumbnail = targetThumbnail;
this.isOutgoing = isOutgoing ?? true;
}
}
@ -736,6 +788,7 @@ let plugin = {
//To override by plugin
const source = {
getHome() { return new ContentPager([], false, {}); },
getShorts() { return new VideoPager([], false, {}); },
enable(config){ },
disable() {},

View file

@ -0,0 +1,319 @@
import kotlin.math.*
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
init {
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
"RGBA channels must be in [0,1]"
}
}
// -- RGB(A) channels stored 01 --
var r: Float = r.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var g: Float = g.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var b: Float = b.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var a: Float = a.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f) }
// -- Int views of RGBA 0255 --
var red: Int
get() = (r * 255).roundToInt()
set(v) { r = (v.coerceIn(0, 255) / 255f) }
var green: Int
get() = (g * 255).roundToInt()
set(v) { g = (v.coerceIn(0, 255) / 255f) }
var blue: Int
get() = (b * 255).roundToInt()
set(v) { b = (v.coerceIn(0, 255) / 255f) }
var alpha: Int
get() = (a * 255).roundToInt()
set(v) { a = (v.coerceIn(0, 255) / 255f) }
// -- HSLA storage & lazy recompute flags --
private var _h: Float = 0f
private var _s: Float = 0f
private var _l: Float = 0f
private var _hslDirty = true
/** Hue [0...360) */
var hue: Float
get() { computeHslIfNeeded(); return _h }
set(v) { setHsl(v, saturation, lightness) }
/** Saturation [0...1] */
var saturation: Float
get() { computeHslIfNeeded(); return _s }
set(v) { setHsl(hue, v, lightness) }
/** Lightness [0...1] */
var lightness: Float
get() { computeHslIfNeeded(); return _l }
set(v) { setHsl(hue, saturation, v) }
private fun computeHslIfNeeded() {
if (!_hslDirty) return
val max = max(max(r, g), b)
val min = min(min(r, g), b)
val d = max - min
_l = (max + min) / 2f
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
_h = when {
d == 0f -> 0f
max == r -> ((g - b) / d % 6f) * 60f
max == g -> (((b - r) / d) + 2f) * 60f
else -> (((r - g) / d) + 4f) * 60f
}.let { if (it < 0f) it + 360f else it }
_hslDirty = false
}
/**
* Set all three HSL channels at once.
* Hue in degrees [0...360), s/l [0...1].
*/
fun setHsl(h: Float, s: Float, l: Float) {
val hh = ((h % 360f) + 360f) % 360f
val cc = (1f - abs(2f * l - 1f)) * s
val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
val m = l - cc / 2f
val (rp, gp, bp) = when {
hh < 60f -> Triple(cc, x, 0f)
hh < 120f -> Triple(x, cc, 0f)
hh < 180f -> Triple(0f, cc, x)
hh < 240f -> Triple(0f, x, cc)
hh < 300f -> Triple(x, 0f, cc)
else -> Triple(cc, 0f, x)
}
r = rp + m; g = gp + m; b = bp + m
_h = hh; _s = s; _l = l; _hslDirty = false
}
/** Return 0xRRGGBBAA int */
fun toRgbaInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
}
/** Return 0xAARRGGBB int */
fun toArgbInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
}
// — Convenience modifiers (chainable) —
/** Lighten by fraction [0...1] */
fun lighten(fraction: Float): CSSColor = apply {
lightness = (lightness + fraction).coerceIn(0f, 1f)
}
/** Darken by fraction [0...1] */
fun darken(fraction: Float): CSSColor = apply {
lightness = (lightness - fraction).coerceIn(0f, 1f)
}
/** Increase saturation by fraction [0...1] */
fun saturate(fraction: Float): CSSColor = apply {
saturation = (saturation + fraction).coerceIn(0f, 1f)
}
/** Decrease saturation by fraction [0...1] */
fun desaturate(fraction: Float): CSSColor = apply {
saturation = (saturation - fraction).coerceIn(0f, 1f)
}
/** Rotate hue by degrees (can be negative) */
fun rotateHue(degrees: Float): CSSColor = apply {
hue = (hue + degrees) % 360f
}
companion object {
/** Create from Android 0xAARRGGBB */
@JvmStatic fun fromArgb(color: Int): CSSColor {
val a = ((color ushr 24) and 0xFF) / 255f
val r = ((color ushr 16) and 0xFF) / 255f
val g = ((color ushr 8) and 0xFF) / 255f
val b = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
/** Create from Android 0xRRGGBBAA */
@JvmStatic fun fromRgba(color: Int): CSSColor {
val r = ((color ushr 24) and 0xFF) / 255f
val g = ((color ushr 16) and 0xFF) / 255f
val b = ((color ushr 8) and 0xFF) / 255f
val a = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
@JvmStatic fun fromAndroidColor(color: Int): CSSColor {
return fromArgb(color)
}
private val NAMED_HEX = mapOf(
"aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
"aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
"bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
"blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
"burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
"chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
"cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
"darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
"darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
"darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
"darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
"darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
"darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
"darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
"dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
"firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
"fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
"gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
"green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
"honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
"indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
"lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
"lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
"lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
"lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
"lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
"lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
"lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
"linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
"mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
"mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
"mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
"midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
"moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
"oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
"orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
"palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
"palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
"peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
"powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
"red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
"saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
"seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
"silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
"slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
"springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
"teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
"turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
"white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
"yellowgreen" to "9ACD32"
)
private val NAMED: Map<String, Int> = NAMED_HEX
.mapValues { (_, hexRgb) ->
// parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
val rgb = hexRgb.toInt(16)
(rgb shl 8) or 0xFF
} + ("transparent" to 0x00000000)
private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
@JvmStatic
fun parseColor(s: String): CSSColor {
val str = s.trim()
// named
NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
// hex
HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
return parseHexPart(part)
}
// rgb/rgba
RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseRgbParts(it.split(',').map(String::trim))
}
// hsl/hsla
HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseHslParts(it.split(',').map(String::trim))
}
error("Cannot parse color: \"$s\"")
}
private fun parseHexPart(p: String): CSSColor {
// expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
val hex = when (p.length) {
3 -> p.map { "$it$it" }.joinToString("") + "FF"
4 -> p.map { "$it$it" }.joinToString("")
6 -> p + "FF"
8 -> p
else -> error("Invalid hex color: #$p")
}
val parsed = hex.toLong(16).toInt()
val alpha = (parsed and 0xFF) shl 24
val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
val argb = alpha or rgbOnly
return fromArgb(argb)
}
private fun parseRgbParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
// r/g/b: "128" → 128/255, "50%" → 0.5
fun channel(ch: String): Float =
if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
else ch.toFloat().coerceIn(0f, 255f) / 255f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
else a.toFloat().coerceIn(0f, 1f)
val r = channel(parts[0])
val g = channel(parts[1])
val b = channel(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(r, g, b, a)
}
private fun parseHslParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
fun hueOf(h: String): Float = when {
h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
else -> h.toFloat()
}
// for s and l you only ever see percentages
fun pct(p: String): Float =
p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) pct(a)
else a.toFloat().coerceIn(0f, 1f)
val h = hueOf(parts[0])
val s = pct(parts[1])
val l = pct(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
}
}
}
fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
fun CSSColor.toAndroidColor(): Int = toArgbInt()

View file

@ -14,7 +14,6 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong
@ -226,6 +225,25 @@ fun Long.toHumanTime(isMs: Boolean): String {
else
return "${prefix}${minsStr}:${secsStr}"
}
fun Long.toHumanDuration(isMs: Boolean): String {
var scaler = 1;
if(isMs)
scaler = 1000;
val v = Math.abs(this);
val hours = Math.max(v/(secondsInHour*scaler), 0);
val mins = Math.max((v % (secondsInHour*scaler)) / (secondsInMinute * scaler), 0);
val minsStr = mins.toString();
val seconds = Math.max(((v % (secondsInHour*scaler)) % (secondsInMinute * scaler))/scaler, 0);
val secsStr = seconds.toString().padStart(2, '0');
val prefix = if (this < 0) { "-" } else { "" };
return listOf(
if(hours > 0) "${hours}h" else null,
if(mins > 0) "${mins}m" else null ,
if(seconds > 0) "${seconds}s" else null
).filterNotNull().joinToString(" ");
}
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
fun String.fixHtmlWhitespace(): Spanned {
@ -357,14 +375,19 @@ private val slds = hashSetOf(".com.ac", ".net.ac", ".gov.ac", ".org.ac", ".mil.a
fun String.matchesDomain(queryDomain: String): Boolean {
if(queryDomain.startsWith(".")) {
val parts = queryDomain.lowercase().split(".");
if(parts.size < 3)
val parts = this.lowercase().split(".");
val queryParts = queryDomain.lowercase().trimStart("."[0]).split(".");
if(queryParts.size < 2)
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
if(parts.size >= 3){
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
if(isSLD && parts.size <= 3)
else {
val possibleDomain = "." + queryParts.joinToString(".");
if(slds.contains(possibleDomain))
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
/*
val isSLD = slds.contains("." + queryParts[queryParts.size - 2] + "." + queryParts[queryParts.size - 1]);
if(isSLD && queryParts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
*/
}
//TODO: Should be safe, but double verify if can't be exploited
@ -376,9 +399,11 @@ fun String.matchesDomain(queryDomain: String): Boolean {
fun String.getSubdomainWildcardQuery(): String {
val domainParts = this.split(".");
val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
if(slds.contains(sldParts))
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
var wildcardDomain = if(domainParts.size > 2)
"." + domainParts.drop(1).joinToString(".")
else
return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
"." + domainParts.joinToString(".");
if(slds.contains(wildcardDomain.lowercase()))
"." + domainParts.joinToString(".");
return wildcardDomain;
}

View file

@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
@ -215,8 +216,13 @@ private fun ByteArray.toInetAddress(): InetAddress {
return InetAddress.getByAddress(this);
}
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
val timeout = 2000
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
ensureNotMainThread()
val timeout = 10000
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
if(addresses.isEmpty())
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
if (addresses.isEmpty()) {
return null;
@ -235,8 +241,11 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
return null;
}
val sortedAddresses: List<InetAddress> = addresses
.sortedBy { addr -> addressScore(addr) }
val sockets: ArrayList<Socket> = arrayListOf();
for (i in addresses.indices) {
for (i in sortedAddresses.indices) {
sockets.add(Socket());
}
@ -244,7 +253,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
var connectedSocket: Socket? = null;
val threads: ArrayList<Thread> = arrayListOf();
for (i in 0 until sockets.size) {
val address = addresses[i];
val address = sortedAddresses[i];
val socket = sockets[i];
val thread = Thread {
try {

View file

@ -1,13 +1,13 @@
package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64UrlToByteArray
import userpackage.Protocol
import kotlin.math.abs
import kotlin.math.min
@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
}
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
val urlData = if (this.startsWith("polycentric://")) {
this.substring("polycentric://".length)
} else this;
val urlBytes = urlData.base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
if (urlInfo.urlType != 4L) {
return null
}
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
return dataLink
}
fun Protocol.Claim.resolveChannelUrl(): String? {
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
fun Protocol.Claim.resolveChannelUrls(): List<String> {
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
addServer(PolycentricCache.SERVER)
}
val exceptions = fullyBackfillServers()
for (pair in exceptions) {
val server = pair.key
val exception = pair.value
StateAnnouncement.instance.registerAnnouncement(
"backfill-failed",
"Backfill failed",
"Failed to backfill server $server. $exception",
AnnouncementType.SESSION_RECURRING
);
Logger.e("Backfill", "Failed to backfill server $server.", exception)
}
}

View file

@ -7,6 +7,9 @@ import java.net.InetAddress
import java.net.URI
import java.net.URISyntaxException
import java.net.URLEncoder
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
//Syntax sugaring
inline fun <reified T> Any.assume(): T?{
@ -33,13 +36,37 @@ fun Boolean?.toYesNo(): String {
fun InetAddress?.toUrlAddress(): String {
return when (this) {
is Inet6Address -> {
"[${toString()}]"
val hostAddr = this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
val index = hostAddr.indexOf('%')
if (index != -1) {
val addrPart = hostAddr.substring(0, index)
val scopeId = hostAddr.substring(index + 1)
"[${addrPart}%25${scopeId}]" // %25 is URL-encoded '%'
} else {
"[$hostAddr]"
}
}
is Inet4Address -> {
toString()
this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
}
else -> {
throw Exception("Invalid address type")
}
}
}
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
}
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
}

View file

@ -2,10 +2,30 @@ package com.futo.platformplayer
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.*
import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.selects.SelectClause0
import kotlinx.coroutines.selects.SelectClause1
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
//V8
@ -24,6 +44,10 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
return handler(this);
}
inline fun V8Value.getSourcePlugin(): V8Plugin? {
return V8Plugin.getPluginFromRuntime(this.v8Runtime);
}
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
if(this !is T)
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
@ -89,7 +113,29 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
.map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T };
}
inline fun V8Plugin.ensureIsBusy() {
this.let {
if (!it.isThreadAlreadyBusy()) {
//throw IllegalStateException("Tried to access V8Plugin without busy");
val stacktrace = Thread.currentThread().stackTrace;
Logger.w("Extensions_V8",
"V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
", " + stacktrace.drop(4)?.firstOrNull().toString() +
", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
", " + stacktrace.drop(6)?.firstOrNull()?.toString()
);
}
}
}
inline fun V8Value.ensureIsBusy() {
this?.getSourcePlugin()?.let {
it.ensureIsBusy();
}
}
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
if(false)
ensureIsBusy();
return when(T::class) {
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
Int::class -> {
@ -146,4 +192,137 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
map.put(prop, obj.getString(prop));
return map;
}
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
val latch = CountDownLatch(1);
var promiseResult: T? = null;
var promiseException: Throwable? = null;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
if(p0 is V8ValueError)
promiseException = ScriptExecutionException(plugin.config, p0.message);
else
promiseResult = p0 as T;
latch.countDown();
}
override fun onRejected(p0: V8Value?) {
promiseException = (NotImplementedError("onRejected promise not implemented.."));
latch.countDown();
}
override fun onCatch(p0: V8Value?) {
promiseException = (NotImplementedError("onCatch promise not implemented.."));
latch.countDown();
}
});
}
plugin.registerPromise(this) {
promiseException = CancellationException("Cancelled by system");
latch.countDown();
}
plugin.unbusy {
latch.await();
}
if(promiseException != null)
throw promiseException!!;
return promiseResult!!;
}
fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T> {
val underlyingDef = CompletableDeferred<T>();
val def = if(this.has("estDuration"))
V8Deferred(underlyingDef,
this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
else
V8Deferred<T>(underlyingDef);
if(def.estDuration > 0)
Logger.i("V8", "Promise with duration: [${def.estDuration}]");
val promise = this;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.complete(p0 as T);
}
override fun onRejected(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
}
override fun onCatch(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
}
});
}
plugin.registerPromise(promise) {
if(def.isActive)
def.cancel("Cancelled by system");
}
return def;
}
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
val newDef = CompletableDeferred<R>()
this.invokeOnCompletion {
if(it != null)
newDef.completeExceptionally(it);
else
newDef.complete(conversion(this@V8Deferred.getCompleted()));
}
return V8Deferred<R>(newDef, estDuration);
}
companion object {
fun <T, R> merge(scope: CoroutineScope, defs: List<V8Deferred<T>>, conversion: (result: List<T>)->R): V8Deferred<R> {
var amount = -1;
for(def in defs)
amount = Math.max(amount, def.estDuration);
val def = scope.async {
val results = defs.map { it.await() };
return@async conversion(results);
}
return V8Deferred(def, amount);
}
}
}
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result as T;
}
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!);
}
return V8Deferred(CompletableDeferred(result as T));
}
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result;
}
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
return result;
}
return V8Deferred(CompletableDeferred(result));
}

View file

@ -29,6 +29,7 @@ import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.AdvancedField
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
@ -175,6 +176,10 @@ class Settings : FragmentedStorageFileJson() {
}
}*/
@FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings")
var advancedSettings: Boolean = false;
@FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings();
@Serializable
@ -205,7 +210,7 @@ class Settings : FragmentedStorageFileJson() {
var home = HomeSettings();
@Serializable
class HomeSettings {
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
@DropdownFieldOptionsId(R.array.feed_style)
var homeFeedStyle: Int = 1;
@ -216,10 +221,16 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL;
}
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
var showHomeFilters: Boolean = true;
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@ -248,12 +259,17 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.feed_style)
var searchFeedStyle: Int = 1;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
var hidefromSearch: Boolean = false;
fun getSearchFeedStyle(): FeedStyle {
if(searchFeedStyle == 0)
@ -269,6 +285,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class ChannelSettings {
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
}
@ -291,16 +308,23 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true;
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true;
@ -331,13 +355,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false;
@AdvancedField
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true;
@AdvancedField
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false;
@AdvancedField
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false;
@ -353,7 +380,7 @@ class Settings : FragmentedStorageFileJson() {
var playback = PlaybackSettings();
@Serializable
class PlaybackSettings {
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
@DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0;
@ -377,6 +404,8 @@ class Settings : FragmentedStorageFileJson() {
else -> null
}
}
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
var preferOriginalAudio: Boolean = true;
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@ -412,21 +441,22 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@AdvancedField
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true;
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
var autoRotate: Int = 2;
@AdvancedField
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
@DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2;
fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
@AdvancedField
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
@DropdownFieldOptionsId(R.array.resume_after_preview)
var resumeAfterPreview: Int = 1;
@ -453,14 +483,10 @@ class Settings : FragmentedStorageFileJson() {
};
}
@AdvancedField
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
var useLiveChatWindow: Boolean = true;
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
var backgroundSwitchToAudio: Boolean = true;
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterLoss: Int = 1;
@ -486,8 +512,97 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false;
@AdvancedField
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true;
@FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23)
@DropdownFieldOptionsId(R.array.seek_offset_duration)
var seekOffset: Int = 2;
fun getSeekOffset(): Long {
return when(seekOffset) {
0 -> 3_000L;
1 -> 5_000L;
2 -> 10_000L;
3 -> 20_000L;
4 -> 30_000L;
5 -> 60_000L;
else -> 10_000L;
}
}
@FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
@DropdownFieldOptionsId(R.array.min_playback_speed)
var minimumPlaybackSpeed: Int = 0;
@FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.max_playback_speed)
var maximumPlaybackSpeed: Int = 2;
@FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.step_playback_speed)
var stepPlaybackSpeed: Int = 1;
fun getPlaybackSpeedStep(): Double {
return when(stepPlaybackSpeed) {
0 -> 0.05
1 -> 0.1
2 -> 0.25
else -> 0.1;
}
}
fun getPlaybackSpeeds(): List<Double> {
val playbackSpeeds = mutableListOf<Double>();
playbackSpeeds.add(1.0);
val minSpeed = when(minimumPlaybackSpeed) {
0 -> 0.25
1 -> 0.5
2 -> 1.0
else -> 0.25
}
val maxSpeed = when(maximumPlaybackSpeed) {
0 -> 2.0
1 -> 2.25
2 -> 3.0
3 -> 4.0
4 -> 5.0
else -> 2.25;
}
var testSpeed = 1.0;
while(testSpeed > minSpeed) {
val nextSpeed = (testSpeed - 0.25) as Double;
testSpeed = Math.max(nextSpeed, minSpeed);
playbackSpeeds.add(testSpeed);
}
testSpeed = 1.0;
while(testSpeed < maxSpeed) {
val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
testSpeed = Math.min(nextSpeed, maxSpeed);
playbackSpeeds.add(testSpeed);
}
playbackSpeeds.sort();
return playbackSpeeds;
}
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
var holdPlaybackSpeed: Int = 4;
fun getHoldPlaybackSpeed(): Double {
return when(holdPlaybackSpeed) {
0 -> 1.0
1 -> 1.25
2 -> 1.5
3 -> 1.75
4 -> 2.0
5 -> 2.25
6 -> 2.5
7 -> 2.75
8 -> 3.0
else -> 2.0
}
}
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
@ -503,6 +618,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
@AdvancedField
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false;
@ -539,10 +655,12 @@ class Settings : FragmentedStorageFileJson() {
var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
@AdvancedField
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true;
@AdvancedField
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
@DropdownFieldOptionsId(R.array.thread_count)
var byteRangeConcurrency: Int = 3;
@ -572,10 +690,21 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true;
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
@AdvancedField
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true;
@AdvancedField
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowLinkLocalIpv4: Boolean = false;
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
@ -643,6 +772,11 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class Plugins {
@AdvancedField
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false;
@AdvancedField
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true;
@ -843,7 +977,23 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
fun viewLicenseStatus() {
SettingsActivity.getActivity()?.let {
try {
if (StatePayment.instance.hasPaid) {
val paymentKey = StatePayment.instance.getPaymentKey()
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first)
} else {
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated")
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show license status dialog", e)
}
}
}
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
fun clearPayment() {
SettingsActivity.getActivity()?.let { context ->
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
@ -861,13 +1011,20 @@ class Settings : FragmentedStorageFileJson() {
var other = Other();
@Serializable
class Other {
@AdvancedField
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true;
@AdvancedField
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
@FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
var watchLaterAddStart: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
var polycentricLocalCache: Boolean = true;
}
@ -905,7 +1062,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = true;
var enabled: Boolean = false;
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
var broadcast: Boolean = false;
@ -915,6 +1072,21 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
var connectLast: Boolean = true;
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
var discoverThroughRelay: Boolean = true;
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
var pairThroughRelay: Boolean = true;
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
var connectThroughRelay: Boolean = true;
@FormField(R.string.connect_local_direct_through_relay, FieldForm.TOGGLE, R.string.connect_local_direct_through_relay_description, 3)
var connectLocalDirectThroughRelay: Boolean = true;
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
var localConnections: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
@ -983,4 +1155,4 @@ class Settings : FragmentedStorageFileJson() {
}
}
//endregion
}
}

View file

@ -1,6 +1,7 @@
package com.futo.platformplayer
import android.content.Context
import android.content.Intent
import android.webkit.CookieManager
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder

View file

@ -5,6 +5,7 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.net.Uri
import android.text.Layout
import android.text.method.ScrollingMovementMethod
@ -199,16 +200,21 @@ class UIDialogs {
dialog.show();
}
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
}
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
builder.setView(view);
builder.setCancelable(defaultCloseAction > -2);
val dialog = builder.create();
registerDialogOpened(dialog);
view.findViewById<ImageView>(R.id.dialog_icon).apply {
this.setImageResource(icon);
if(animated)
this.drawable.assume<Animatable, Unit> { it.start() };
}
view.findViewById<TextView>(R.id.dialog_text).apply {
this.text = text;
@ -275,6 +281,7 @@ class UIDialogs {
registerDialogClosed(dialog);
}
dialog.show();
return dialog;
}
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
@ -312,7 +319,11 @@ class UIDialogs {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), {
retryAction?.invoke();
try {
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
}, UIDialogs.ActionStyle.PRIMARY)
);
else
@ -326,7 +337,11 @@ class UIDialogs {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), {
retryAction?.invoke();
try {
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
}, UIDialogs.ActionStyle.PRIMARY)
);
}
@ -368,8 +383,8 @@ class UIDialogs {
}
}
fun showChangelogDialog(context: Context, lastVersion: Int) {
val dialog = ChangelogDialog(context);
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
val dialog = ChangelogDialog(context, changelogs);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
@ -409,7 +424,7 @@ class UIDialogs {
}
fun showCastingDialog(context: Context) {
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
val d = StateCasting.instance.activeDevice;
if (d != null) {
val dialog = ConnectedCastingDialog(context);
@ -417,6 +432,7 @@ class UIDialogs {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
} else {
@ -429,21 +445,24 @@ class UIDialogs {
if (c is Activity) {
dialog.setOwnerActivity(c);
}
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
}
fun showCastingTutorialDialog(context: Context) {
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
val dialog = CastingHelpDialog(context);
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
fun showCastingAddDialog(context: Context) {
fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) {
val dialog = CastingAddDialog(context);
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}

View file

@ -4,8 +4,14 @@ import android.app.NotificationManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
@ -37,6 +43,9 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.parsers.HLS.MediaRendition
import com.futo.platformplayer.parsers.HLS.StreamInfo
import com.futo.platformplayer.parsers.HLS.VariantPlaylistReference
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
@ -63,6 +72,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import androidx.core.net.toUri
class UISlideOverlays {
companion object {
@ -79,6 +90,36 @@ class UISlideOverlays {
return menu;
}
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
UISlideOverlays.showOverlay(container, "Queue options", null, {
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
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);
addPlaylistOverlay.onOK.subscribe {
val text = nameInput.text.trim()
if (text.isBlank()) {
return@subscribe;
}
addPlaylistOverlay.hide();
nameInput.deactivate();
nameInput.clear();
StatePlayer.instance.saveQueueAsPlaylist(text);
UIDialogs.appToast("Playlist [${text}] created");
};
addPlaylistOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addPlaylistOverlay.show();
nameInput.activate();
}, false));
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>();
@ -88,115 +129,163 @@ class UISlideOverlays {
val originalVideo = subscription.doFetchVideos;
val originalPosts = subscription.doFetchPosts;
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
val menu = SlideUpMenuOverlay(
container.context,
container,
"Subscription Settings",
null,
true,
listOf()
);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities();
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities();
withContext(Dispatchers.Main) {
items.addAll(listOf(
SlideUpMenuItem(
container.context,
R.drawable.ic_notifications,
"Notifications",
"",
tag = "notifications",
call = {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
},
invokeParent = 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);
withContext(Dispatchers.Main) {
items.addAll(
listOf(
SlideUpMenuItem(
container.context,
R.drawable.ic_notifications,
"Notifications",
"",
tag = "notifications",
call = {
subscription.doNotifications =
menu?.selectOption(null, "notifications", true, true)
?: subscription.doNotifications;
},
invokeParent = 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",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()),
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
container.context,
R.drawable.ic_live_tv,
"Livestreams",
"Check for livestreams",
tag = "fetchLive",
call = {
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Streams",
"Check for streams",
tag = "fetchStreams",
call = {
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Videos",
"Check for videos",
tag = "fetchVideos",
call = {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
},
invokeParent = false
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Content",
"Check for content",
tag = "fetchVideos",
call = {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
container.context,
R.drawable.ic_chat,
"Posts",
"Check for posts",
tag = "fetchPosts",
call = {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
},
invokeParent = false
) else null/*,,
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",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()
),
if (capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
container.context,
R.drawable.ic_live_tv,
"Livestreams",
"Check for livestreams",
tag = "fetchLive",
call = {
subscription.doFetchLive =
menu?.selectOption(null, "fetchLive", true, true)
?: subscription.doFetchLive;
},
invokeParent = false
) else null,
if (capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Streams",
"Check for streams",
tag = "fetchStreams",
call = {
subscription.doFetchStreams =
menu?.selectOption(null, "fetchStreams", true, true)
?: subscription.doFetchStreams;
},
invokeParent = false
) else null,
if (capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Videos",
"Check for videos",
tag = "fetchVideos",
call = {
subscription.doFetchVideos =
menu?.selectOption(null, "fetchVideos", true, true)
?: subscription.doFetchVideos;
},
invokeParent = false
) else if (capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Content",
"Check for content",
tag = "fetchVideos",
call = {
subscription.doFetchVideos =
menu?.selectOption(null, "fetchVideos", true, true)
?: subscription.doFetchVideos;
},
invokeParent = false
) else null,
if (capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
container.context,
R.drawable.ic_chat,
"Posts",
"Check for posts",
tag = "fetchPosts",
call = {
subscription.doFetchPosts =
menu?.selectOption(null, "fetchPosts", true, true)
?: subscription.doFetchPosts;
},
invokeParent = false
) else null/*,,
SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription",
@ -204,61 +293,82 @@ class UISlideOverlays {
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
showCreateSubscriptionGroup(container, subscription.channel);
}, false)*/
).filterNotNull());
).filterNotNull()
);
menu.setItems(items);
menu.setItems(items);
if(subscription.doNotifications)
menu.selectOption(null, "notifications", true, true);
if(subscription.doFetchLive)
menu.selectOption(null, "fetchLive", true, true);
if(subscription.doFetchStreams)
menu.selectOption(null, "fetchStreams", true, true);
if(subscription.doFetchVideos)
menu.selectOption(null, "fetchVideos", true, true);
if(subscription.doFetchPosts)
menu.selectOption(null, "fetchPosts", true, true);
if (subscription.doNotifications)
menu.selectOption(null, "notifications", true, true);
if (subscription.doFetchLive)
menu.selectOption(null, "fetchLive", true, true);
if (subscription.doFetchStreams)
menu.selectOption(null, "fetchStreams", true, true);
if (subscription.doFetchVideos)
menu.selectOption(null, "fetchVideos", true, true);
if (subscription.doFetchPosts)
menu.selectOption(null, "fetchPosts", true, true);
menu.onOK.subscribe {
subscription.save();
menu.hide(true);
menu.onOK.subscribe {
subscription.save();
menu.hide(true);
if(subscription.doNotifications && !originalNotif) {
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 (subscription.doNotifications && !originalNotif) {
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");
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 {
subscription.doNotifications = originalNotif;
subscription.doFetchLive = originalLive;
subscription.doFetchStreams = originalStream;
subscription.doFetchVideos = originalVideo;
subscription.doFetchPosts = originalPosts;
};
};
menu.onCancel.subscribe {
subscription.doNotifications = originalNotif;
subscription.doFetchLive = originalLive;
subscription.doFetchStreams = originalStream;
subscription.doFetchVideos = originalVideo;
subscription.doFetchPosts = originalPosts;
};
menu.setOk("Save");
menu.setOk("Save");
menu.show();
menu.show();
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show subscription overlay.", e)
}
}
@ -269,6 +379,7 @@ class UISlideOverlays {
}
@OptIn(UnstableApi::class)
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(LoaderView(container.context))
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
@ -280,6 +391,8 @@ class UISlideOverlays {
val masterPlaylistContent = masterPlaylistResponse.body?.string()
?: throw Exception("Master playlist content is empty")
val resolvedPlaylistUrl = masterPlaylistResponse.url
val videoButtons = arrayListOf<SlideUpMenuItem>()
val audioButtons = arrayListOf<SlideUpMenuItem>()
//TODO: Implement subtitles
@ -292,53 +405,103 @@ class UISlideOverlays {
val masterPlaylist: HLS.MasterPlaylist
try {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
.parse(sourceUrl.toUri(), inputStream)
masterPlaylist.getAudioSources().forEach { it ->
if (playlist is HlsMediaPlaylist) {
if (source is IHLSManifestAudioSource) {
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
val estSize = VideoHelper.estimateSourceSize(variant);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
variant.name,
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + variant.codec).trim(),
tag = variant,
call = {
selectedAudioVariant = variant
slideUpMenuOverlay.selectOption(audioButtons, variant)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
} else {
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
val estSize = VideoHelper.estimateSourceSize(variant);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
variant.name,
"${variant.width}x${variant.height}",
(prefix + variant.codec).trim(),
tag = variant,
call = {
selectedVideoVariant = variant
slideUpMenuOverlay.selectOption(videoButtons, variant)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
}
} else if (playlist is HlsMultivariantPlaylist) {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
masterPlaylist.getAudioSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
}
/*masterPlaylist.getSubtitleSources().forEach { it ->
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedSubtitleVariant = it
slideUpMenuOverlay.selectOption(subtitleButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
}
}, false))
}*/
/*masterPlaylist.getSubtitleSources().forEach { it ->
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedSubtitleVariant = it
slideUpMenuOverlay.selectOption(subtitleButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}, false))
}*/
masterPlaylist.getVideoSources().forEach {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
masterPlaylist.getVideoSources().forEach {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
}
}
val newItems = arrayListOf<View>()
@ -366,11 +529,11 @@ class UISlideOverlays {
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
withContext(Dispatchers.Main) {
if (source is IHLSManifestSource) {
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null)
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
slideUpMenuOverlay.hide()
} else if (source is IHLSManifestAudioSource) {
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
slideUpMenuOverlay.hide()
} else {
@ -417,7 +580,7 @@ class UISlideOverlays {
}
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
listOf(listOf(SlideUpMenuItem(
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
container.context.getString(R.string.none),
@ -430,7 +593,7 @@ class UISlideOverlays {
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)) +
)) else listOf()) +
videoSources
.filter { it.isDownloadable() }
.map {
@ -652,6 +815,10 @@ class UISlideOverlays {
}
}
}
if(!Settings.instance.downloads.shouldDownload()) {
UIDialogs.appToast("Download will start when you're back on wifi.\n" +
"(You can change this in settings)", true);
}
}
};
return menu.apply { show() };
@ -895,7 +1062,8 @@ class UISlideOverlays {
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists();
}))
);
@ -906,7 +1074,7 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf(
if(!isLimited)
if(!isLimited && !video.isLive)
SlideUpMenuItem(
container.context,
R.drawable.ic_download,
@ -947,26 +1115,30 @@ class UISlideOverlays {
+ actions).filterNotNull()
));
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
SlideUpMenuItem(container.context,
SlideUpMenuGroup(
container.context, container.context.getString(R.string.add_to), "addto",
SlideUpMenuItem(
container.context,
R.drawable.ic_queue_add,
container.context.getString(R.string.add_to_queue),
"${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_watchlist_add,
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_history,
container.context.getString(R.string.add_to_history),
"Mark as watched",
tag = "history",
call = { StateHistory.instance.markAsWatched(video); }),
));
));
val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(
@ -991,7 +1163,8 @@ class UISlideOverlays {
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
StatePlaylists.instance.addToPlaylist(playlist.id, video);
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists();
}));
}
@ -1018,7 +1191,8 @@ class UISlideOverlays {
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists();
}))
);
@ -1028,20 +1202,28 @@ class UISlideOverlays {
val queue = StatePlayer.instance.getQueue();
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(container.context,
SlideUpMenuGroup(
container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(
container.context,
R.drawable.ic_queue_add,
container.context.getString(R.string.queue),
"${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_watchlist_add,
StatePlayer.TYPE_WATCHLATER,
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
)
call = {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false);
else
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
}),
)
);
val playlistItems = arrayListOf<SlideUpMenuItem>();
@ -1067,7 +1249,8 @@ class UISlideOverlays {
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
StatePlaylists.instance.addToPlaylist(playlist.id, video);
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists();
}));
}
@ -1078,8 +1261,8 @@ class UISlideOverlays {
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>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
overlay.show();
return overlay;
}
@ -1109,7 +1292,7 @@ class UISlideOverlays {
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
tag = "",
call = {
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
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
.filter { it != null }
@ -1117,7 +1300,7 @@ class UISlideOverlays {
.toList();
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
}
});
},
invokeParent = false
))
@ -1125,29 +1308,40 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
}
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
val selection: MutableList<Any> = mutableListOf();
var overlay: SlideUpMenuOverlay? = null;
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
options.map { SlideUpMenuItem(
listOf(
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
).filterNotNull() +
(options.map { SlideUpMenuItem(
container.context,
R.drawable.ic_move_up,
it.first,
"",
tag = it.second,
call = {
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
if(overlay!!.selectOption(null, it.second, true, true)) {
if(!selection.contains(it.second))
if(!selection.contains(it.second)) {
selection.add(it.second);
} else
if(overlayItem != null) {
overlayItem.setSubText(selection.indexOf(it.second).toString());
}
}
} else {
selection.remove(it.second);
if(overlayItem != null) {
overlayItem.setSubText("");
}
}
},
invokeParent = false
)
});
}));
overlay.onOK.subscribe {
onOrdered.invoke(selection);
overlay.hide();

View file

@ -27,14 +27,23 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.ByteArrayInputStream
import java.io.File
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InterfaceAddress
import java.net.NetworkInterface
import java.net.SocketException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.SecureRandom
import java.time.OffsetDateTime
import java.util.*
import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String {
@ -66,7 +75,14 @@ fun warnIfMainThread(context: String) {
}
fun ensureNotMainThread() {
if (Looper.myLooper() == Looper.getMainLooper()) {
val isMainLooper = try {
Looper.myLooper() == Looper.getMainLooper()
} catch (e: Throwable) {
//Ignore, for unit tests where its not mocked
false
}
if (isMainLooper) {
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
throw IllegalStateException("Cannot run on main thread")
}
@ -269,7 +285,7 @@ fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
}
}
if(newIndex < 0)
return originalArr.size;
return newArr.size;
else
return newIndex;
}
@ -279,3 +295,167 @@ fun ByteBuffer.toUtf8String(): String {
get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8)
}
fun generateReadablePassword(length: Int): String {
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
val secureRandom = SecureRandom()
val randomBytes = ByteArray(length)
secureRandom.nextBytes(randomBytes)
val sb = StringBuilder(length)
for (byte in randomBytes) {
val index = (byte.toInt() and 0xFF) % validChars.length
sb.append(validChars[index])
}
return sb.toString()
}
fun ByteArray.toGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val gzipTimeStart = OffsetDateTime.now();
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(this)
}
val result = outputStream.toByteArray();
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
return result;
}
fun ByteArray.fromGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val inputStream = ByteArrayInputStream(this)
val outputStream = ByteArrayOutputStream()
GZIPInputStream(inputStream).use { gzip ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (gzip.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
return outputStream.toByteArray()
}
fun findCandidateAddresses(): List<InetAddress> {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.sortedWith(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
).map { it.second.address }
}
fun findPreferredAddress(): InetAddress? {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.minWithOrNull(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
)?.second?.address
}
private fun isUsableInterface(nif: NetworkInterface): Boolean {
val name = nif.name.lowercase()
return try {
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
nif.isUp
&& !nif.isLoopback
&& !nif.isPointToPoint
&& !nif.isVirtual
&& !name.startsWith("docker")
&& !name.startsWith("veth")
&& !name.startsWith("br-")
&& !name.startsWith("virbr")
&& !name.startsWith("vmnet")
&& !name.startsWith("tun")
&& !name.startsWith("tap")
} catch (e: SocketException) {
false
}
}
private fun isUsableAddress(addr: InetAddress): Boolean {
return when {
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
addr.isLoopbackAddress -> false
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
addr.isMulticastAddress -> false
else -> true
}
}
private fun interfaceScore(nif: NetworkInterface): Int {
val name = nif.name.lowercase()
return when {
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
name.startsWith("eth") || name.contains("ethernet") -> 0
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
name.contains("wi-fi") || name.contains("wifi") -> 1
else -> 2
}
}
fun addressScore(addr: InetAddress): Int {
return when (addr) {
is Inet4Address -> {
val octets = addr.address.map { it.toInt() and 0xFF }
when {
octets[0] == 10 -> 0 // 10/8
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.1631/12
else -> 1 // public IPv4
}
}
is Inet6Address -> {
// ULA (fc00::/7) vs global vs others
val b0 = addr.address[0].toInt() and 0xFF
when {
(b0 and 0xFE) == 0xFC -> 2 // ULA
(b0 and 0xE0) == 0x20 -> 3 // global
else -> 4
}
}
else -> Int.MAX_VALUE
}
}
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)

View file

@ -10,11 +10,13 @@ import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.google.zxing.integration.android.IntentIntegrator
class AddSourceOptionsActivity : AppCompatActivity() {
lateinit var _buttonBack: ImageButton;
lateinit var _overlayContainer: FrameLayout;
lateinit var _buttonQR: BigButton;
lateinit var _buttonBrowse: BigButton;
lateinit var _buttonURL: BigButton;
@ -54,6 +56,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
setContentView(R.layout.activity_add_source_options);
setNavigationBarColorAndIcons();
_overlayContainer = findViewById(R.id.overlay_container);
_buttonBack = findViewById(R.id.button_back);
_buttonQR = findViewById(R.id.option_qr);
@ -81,7 +84,25 @@ class AddSourceOptionsActivity : AppCompatActivity() {
}
_buttonURL.onClick.subscribe {
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
val content = nameInput.text;
val url = if (content.startsWith("https://")) {
content
} else if (content.startsWith("grayjay://plugin/")) {
content.substring("grayjay://plugin/".length)
} else {
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
return@showOverlay;
}
val intent = Intent(this, AddSourceActivity::class.java).apply {
data = Uri.parse(url);
};
startActivity(intent);
}, nameInput)
}
}
}

View file

@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
companion object {
private val TAG = "LoginActivity";
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
private var _callback: ((SourceAuth?) -> Unit)? = null;

View file

@ -1,12 +1,15 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.UiModeManager
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
@ -19,6 +22,7 @@ import android.widget.ImageView
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.app.ActivityCompat
@ -28,6 +32,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStateAtLeast
import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
@ -36,7 +41,9 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
@ -55,6 +62,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsF
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
@ -63,7 +71,9 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
@ -104,9 +114,10 @@ import java.io.PrintWriter
import java.io.StringWriter
import java.lang.reflect.InvocationTargetException
import java.util.LinkedList
import java.util.Queue
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
class MainActivity : AppCompatActivity, IWithResultLauncher {
//TODO: Move to dimensions
@ -142,6 +153,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//Frags Main
lateinit var _fragMainHome: HomeFragment;
lateinit var _fragPostDetail: PostDetailFragment;
lateinit var _fragArticleDetail: ArticleDetailFragment;
lateinit var _fragWebDetail: WebDetailFragment;
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
@ -157,6 +170,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment;
lateinit var _fragHistory: HistoryFragment;
lateinit var _fragShorts: ShortsFragment;
lateinit var _fragSourceDetail: SourceDetailFragment;
lateinit var _fragDownloads: DownloadsFragment;
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
@ -171,7 +185,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragVideoDetail: VideoDetailFragment;
//State
private val _queue: Queue<Pair<MainFragment, Any?>> = LinkedList();
private val _queue: LinkedList<Pair<MainFragment, Any?>> = LinkedList();
lateinit var fragCurrent: MainFragment private set;
private var _parameterCurrent: Any? = null;
@ -181,6 +195,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private var _isVisible = true;
private var _wasStopped = false;
private var _privateModeEnabled = false
private var _pictureInPictureEnabled = false
private var _isFullscreen = false
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@ -192,7 +209,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
try {
runBlocking {
lifecycleScope.launch {
handleUrlAll(content)
}
} catch (e: Throwable) {
@ -202,6 +219,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
val mainId = UUID.randomUUID().toString().substring(0, 5)
constructor() : super() {
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(
@ -253,11 +272,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
@UnstableApi
override fun onCreate(savedInstanceState: Bundle?) {
Logger.i(TAG, "MainActivity Starting");
StateApp.instance.setGlobalContext(this, lifecycleScope);
Logger.w(TAG, "MainActivity Starting [$mainId]");
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
StateApp.instance.mainAppStarting(this);
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val uiMode = getSystemService(UiModeManager::class.java)
uiMode.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
}
setContentView(R.layout.activity_main);
setNavigationBarColorAndIcons();
if (Settings.instance.playback.allowVideoToGoUnderCutout)
@ -265,7 +288,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity);
try {
StatePlatform.instance.updateAvailableClients(this@MainActivity);
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in updateAvailableClients", e)
}
}
//Preload common files to memory
@ -309,8 +336,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainPlaylist = PlaylistFragment.newInstance();
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
_fragPostDetail = PostDetailFragment.newInstance();
_fragArticleDetail = ArticleDetailFragment.newInstance();
_fragWebDetail = WebDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance();
_fragShorts = ShortsFragment.newInstance();
_fragSourceDetail = SourceDetailFragment.newInstance();
_fragDownloads = DownloadsFragment();
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
@ -350,22 +380,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSubscriptionsFeed.setPreviewsEnabled(true);
_fragContainerVideoDetail.visibility = View.INVISIBLE;
updateSegmentPaddings();
updatePrivateModeVisibility()
};
_buttonIncognito = findViewById(R.id.incognito_button);
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
updatePrivateModeVisibility()
StateApp.instance.privateModeChanged.subscribe {
//Messing with visibility causes some issues with layout ordering?
if (it) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
_privateModeEnabled = it
updatePrivateModeVisibility()
}
_buttonIncognito.setOnClickListener {
if (!StateApp.instance.privateMode)
return@setOnClickListener;
@ -382,19 +408,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
};
_fragVideoDetail.onFullscreenChanged.subscribe {
Logger.i(TAG, "onFullscreenChanged ${it}");
_isFullscreen = it
updatePrivateModeVisibility()
}
if (it) {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
} else {
if (StateApp.instance.privateMode) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
_fragVideoDetail.onMinimize.subscribe {
updatePrivateModeVisibility()
}
_fragVideoDetail.onMaximized.subscribe {
updatePrivateModeVisibility()
}
StatePlayer.instance.also {
@ -442,6 +465,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainPlaylist.topBar = _fragTopBarNavigation;
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
_fragPostDetail.topBar = _fragTopBarNavigation;
_fragArticleDetail.topBar = _fragTopBarNavigation;
_fragWebDetail.topBar = _fragTopBarNavigation;
_fragWatchlist.topBar = _fragTopBarNavigation;
_fragHistory.topBar = _fragTopBarNavigation;
_fragSourceDetail.topBar = _fragTopBarNavigation;
@ -586,6 +611,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}, UIDialogs.ActionStyle.PRIMARY)
)
}
//startActivity(Intent(this, TestActivity::class.java))
}
/*
@ -609,8 +636,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
}*/
private var _qrCodeLoadingDialog: AlertDialog? = null
fun showUrlQrCodeScanner() {
try {
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
"Launching QR scanner",
"Make sure your camera is enabled", null, -2,
UIDialogs.Action("Close", {
_qrCodeLoadingDialog?.dismiss()
_qrCodeLoadingDialog = null
}));
val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code))
@ -626,21 +663,36 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
@OptIn(UnstableApi::class)
private fun updatePrivateModeVisibility() {
if (_privateModeEnabled && (_fragVideoDetail.state == State.CLOSED || !_pictureInPictureEnabled && !_isFullscreen)) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
_buttonIncognito.translationY = if (_fragVideoDetail.state == State.MINIMIZED) -60.dp(resources).toFloat() else 0f
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
override fun onResume() {
super.onResume();
Logger.v(TAG, "onResume")
Logger.w(TAG, "onResume [$mainId]")
_isVisible = true;
}
override fun onPause() {
super.onPause();
Logger.v(TAG, "onPause")
Logger.w(TAG, "onPause [$mainId]")
_isVisible = false;
_qrCodeLoadingDialog?.dismiss()
_qrCodeLoadingDialog = null
}
override fun onStop() {
super.onStop()
Logger.v(TAG, "_wasStopped = true");
Logger.w(TAG, "onStop [$mainId]");
_wasStopped = true;
}
@ -674,7 +726,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"VIDEO" -> {
val url = intent.getStringExtra("VIDEO");
navigate(_fragVideoDetail, url);
navigateWhenReady(_fragVideoDetail, url);
}
"IMPORT_OPTIONS" -> {
@ -692,11 +744,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"Sources" -> {
runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
navigate(_fragMainSources);
navigateWhenReady(_fragMainSources);
}
};
"BROWSE_PLUGINS" -> {
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
navigateWhenReady(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let {
if (it is MainActivity) {
@ -714,8 +766,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
try {
if (targetData != null) {
runBlocking {
handleUrlAll(targetData)
lifecycleScope.launch(Dispatchers.Main) {
try {
handleUrlAll(targetData)
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
}
}
}
} catch (ex: Throwable) {
@ -743,10 +799,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
startActivity(intent);
} else if (url.startsWith("grayjay://video/")) {
val videoUrl = url.substring("grayjay://video/".length);
navigate(_fragVideoDetail, videoUrl);
navigateWhenReady(_fragVideoDetail, videoUrl);
} else if (url.startsWith("grayjay://channel/")) {
val channelUrl = url.substring("grayjay://channel/".length);
navigate(_fragMainChannel, channelUrl);
navigateWhenReady(_fragMainChannel, channelUrl);
}
}
@ -812,29 +868,29 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return withContext(Dispatchers.IO) {
Logger.i(TAG, "handleUrl(url=$url) on IO");
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
if (StatePlatform.instance.hasEnabledContentClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found video client");
lifecycleScope.launch(Dispatchers.Main) {
withContext(Dispatchers.Main) {
if (position > 0)
navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
navigateWhenReady(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
else
navigate(_fragVideoDetail, url);
navigateWhenReady(_fragVideoDetail, url);
_fragVideoDetail.maximizeVideoDetail(true);
}
return@withContext true;
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found channel client");
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainChannel, url);
withContext(Dispatchers.Main) {
navigateWhenReady(_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(_fragMainRemotePlaylist, url);
withContext(Dispatchers.Main) {
navigateWhenReady(_fragMainRemotePlaylist, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
@ -1046,18 +1102,33 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
_pictureInPictureEnabled = isInPictureInPictureMode
updatePrivateModeVisibility()
}
override fun onDestroy() {
super.onDestroy();
Logger.v(TAG, "onDestroy")
StateApp.instance.mainAppDestroyed(this);
Logger.w(TAG, "onDestroy [$mainId]")
StateApp.instance.mainAppDestroyed(this, mainId);
}
inline fun <reified T> isFragmentActive(): Boolean {
return fragCurrent is T;
}
fun navigateWhenReady(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
navigate(segment, parameter, withHistory, isBack)
} else {
lifecycleScope.launch {
lifecycle.withStateAtLeast(Lifecycle.State.RESUMED) {
navigate(segment, parameter, withHistory, isBack)
}
}
}
}
/**
* 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
@ -1119,7 +1190,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
fragBeforeOverlay = fragCurrent;
fragCurrent = segment;
_parameterCurrent = parameter;
}
@ -1182,8 +1252,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
PlaylistFragment::class -> _fragMainPlaylist as T;
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
PostDetailFragment::class -> _fragPostDetail as T;
ArticleDetailFragment::class -> _fragArticleDetail as T;
WebDetailFragment::class -> _fragWebDetail as T;
WatchLaterFragment::class -> _fragWatchlist as T;
HistoryFragment::class -> _fragHistory as T;
ShortsFragment::class -> _fragShorts as T;
SourceDetailFragment::class -> _fragSourceDetail as T;
DownloadsFragment::class -> _fragDownloads as T;
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;
@ -1277,7 +1350,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (toast.long)
delay(5000);
else
delay(3000);
delay(2500);
}
Logger.i(TAG, "Ending appToast loop");
lifecycleScope.launch(Dispatchers.Main) {

View file

@ -14,10 +14,12 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.ContentType
@ -29,6 +31,9 @@ import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol
import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo
@ -39,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView;
private lateinit var _loader: View
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@ -49,24 +55,47 @@ class PolycentricBackupActivity : AppCompatActivity() {
setContentView(R.layout.activity_polycentric_backup);
setNavigationBarColorAndIcons();
_buttonShare = findViewById(R.id.button_share);
_buttonCopy = findViewById(R.id.button_copy);
_imageQR = findViewById(R.id.image_qr);
_textQR = findViewById(R.id.text_qr);
_buttonShare = findViewById(R.id.button_share)
_buttonCopy = findViewById(R.id.button_copy)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_qr)
_loader = findViewById(R.id.progress_loader)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
};
_exportBundle = createExportBundle();
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
try {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
_imageQR.setImageBitmap(qrCodeBitmap);
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
_imageQR.visibility = View.INVISIBLE;
_textQR.visibility = View.INVISIBLE;
lifecycleScope.launch {
try {
val pair = withContext(Dispatchers.IO) {
val bundle = createExportBundle()
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
).toInt()
val qr = generateQRCode(bundle, dimension, dimension)
Pair(bundle, qr)
}
_exportBundle = pair.first
_imageQR.setImageBitmap(pair.second)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
} finally {
_loader.visibility = View.GONE
}
}
_buttonShare.onClick.subscribe {

View file

@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
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.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.LoaderView
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(PolycentricCache.SERVER);
processHandle.addServer(ApiMethods.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {

View file

@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
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.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret
@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
}
StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
processHandle.fullyBackfillClient(ApiMethods.SERVER);
withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();

View file

@ -21,10 +21,8 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons
@ -32,8 +30,10 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
processHandle.fullyBackfillClient(ApiMethods.SERVER)
withContext(Dispatchers.Main) {
updateUI();

View file

@ -9,6 +9,8 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
@ -29,6 +31,16 @@ class SyncHomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (StateApp.instance.contextOrNull == null) {
Logger.w(TAG, "No main activity, restarting main.")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
finish()
return
}
setContentView(R.layout.activity_sync_home)
setNavigationBarColorAndIcons()
@ -54,7 +66,6 @@ class SyncHomeActivity : AppCompatActivity() {
val view = _viewMap[publicKey]
if (!session.isAuthorized) {
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(publicKey)
}
return@launch
@ -89,6 +100,20 @@ class SyncHomeActivity : AppCompatActivity() {
updateEmptyVisibility()
}
}
StateSync.instance.confirmStarted(this, onStarted = {
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
}
if (StateSync.instance.syncService?.relayConnected == false) {
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
}
if (StateSync.instance.syncService?.serverSocketStarted == false) {
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
}
}, onNotStarted = {
finish()
})
}
override fun onDestroy() {
@ -100,9 +125,12 @@ class SyncHomeActivity : AppCompatActivity() {
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
.setName(publicKey)
.setStatus(if (connected) "Connected" else "Disconnected")
val authorized = session?.isAuthorized ?: false
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key?
.setStatus(if (connected && authorized) "Connected" else "Disconnected or unauthorized")
return syncDeviceView
}

View file

@ -83,6 +83,7 @@ class SyncPairActivity : AppCompatActivity() {
_layoutPairingSuccess.setOnClickListener {
_layoutPairingSuccess.visibility = View.GONE
finish()
}
_layoutPairingError.setOnClickListener {
_layoutPairingError.visibility = View.GONE
@ -109,11 +110,17 @@ class SyncPairActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
StateSync.instance.connect(deviceInfo) { session, complete, message ->
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
lifecycleScope.launch(Dispatchers.Main) {
if (complete) {
_layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
if (complete != null) {
if (complete) {
_layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
} else {
_textError.text = message
_layoutPairingError.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
}
} else {
_textPairingStatus.text = message
}
@ -122,7 +129,11 @@ class SyncPairActivity : AppCompatActivity() {
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
_layoutPairingError.visibility = View.VISIBLE
_textError.text = e.message
if(e.message == "Failed to connect") {
_textError.text = "Failed to connect.\n\nThis may be due to not being on the same network, due to firewall, or vpn.\nSync currently operates only over local direct connections."
}
else
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
}
@ -133,8 +144,6 @@ class SyncPairActivity : AppCompatActivity() {
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
} finally {
_layoutPairing.visibility = View.GONE
}
}

View file

@ -67,11 +67,18 @@ class SyncShowPairingCodeActivity : AppCompatActivity() {
}
val ips = getIPs()
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
val publicKey = StateSync.instance.syncService?.publicKey
val pairingCode = StateSync.instance.syncService?.pairingCode
if (publicKey == null || pairingCode == null) {
setCode("Public key or pairing code was not known, is sync enabled?")
} else {
val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, pairingCode)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
}
}
fun setCode(code: String?) {

View file

@ -2,12 +2,24 @@ package com.futo.platformplayer.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.views.TargetTapLoaderView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
view.startLoader(10000)
lifecycleScope.launch {
delay(5000)
view.startLoader()
}
}
companion object {

View file

@ -5,6 +5,8 @@ import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.FragmentedStorage
import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
@ -63,7 +65,7 @@ open class ManagedHttpClient {
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder;
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
trustAllCertificates(builder);
client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
@ -88,6 +90,7 @@ open class ManagedHttpClient {
}
fun tryHead(url: String): Map<String, String>? {
ensureNotMainThread()
try {
val result = head(url);
if(result.isOk)
@ -102,7 +105,7 @@ open class ManagedHttpClient {
}
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
ensureNotMainThread()
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.url(url);
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
@ -298,6 +301,7 @@ open class ManagedHttpClient {
}
fun send(msg: String) {
ensureNotMainThread()
socket.send(msg);
}

View file

@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
current += bytesToSend.toLong()
if (current >= end) {
if (current > end) {
Logger.i(TAG, "Expected amount of bytes sent")
break
}

View file

@ -1,5 +1,6 @@
package com.futo.platformplayer.api.media
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -12,6 +13,7 @@ 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.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
@ -35,6 +37,11 @@ interface IPlatformClient {
*/
fun getHome(): IPager<IPlatformContent>
/**
* Gets the shorts feed
*/
fun getShorts(): IPager<IPlatformVideo>
//Search
/**
* Gets search suggestion for the provided query string
@ -66,6 +73,11 @@ interface IPlatformClient {
*/
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
/**
* Searches for channels and returns a content pager
*/
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
//Video Pages
/**
@ -170,6 +182,10 @@ interface IPlatformClient {
* Retrieves the subscriptions of the currently logged in user
*/
fun getUserSubscriptions(): Array<String>;
/**
* Retrieves the history of the currently logged in user
*/
fun getUserHistory(): IPager<IPlatformContent>;
fun isClaimTypeSupported(claimType: Int): Boolean;

View file

@ -11,6 +11,7 @@ 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.LiveEventEmojis
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSVODEventPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.BatchedTaskHandler
import com.futo.platformplayer.logging.Logger
@ -26,12 +27,17 @@ class LiveChatManager {
private val _emojiCache: EmojiCache = EmojiCache();
private val _pager: IPager<IPlatformLiveEvent>?;
private var _position: Long = 0;
private var _eventsPosition: Long = 0;
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
private var _startCounter = 0;
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
val isVOD get() = _pager is JSVODEventPager;
var viewCount: Long = 0
private set;
@ -39,8 +45,24 @@ class LiveChatManager {
_scope = scope;
_pager = pager;
viewCount = initialViewCount;
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
handleEvents(pager.getResults());
if(pager is JSVODEventPager)
handleEvents(listOf(LiveEventComment("SYSTEM", null, "VOD chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
else
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
if(pager is JSVODEventPager) {
var replayResults = pager.getResults().filter { it.time > _eventsPosition || it is LiveEventEmojis };
//TODO: Remove this once dripfeed is done properly
replayResults = replayResults.filter{ it.time < _eventsPosition + 1500 || it is LiveEventEmojis };
if(replayResults.size > 0) {
_eventsPosition = replayResults.maxOf { it.time };
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
}
else
_eventsPosition = _eventsPosition + 1500;
}
else
handleEvents(pager.getResults());
}
fun start() {
@ -52,6 +74,10 @@ class LiveChatManager {
_startCounter++;
}
fun setVideoPosition(ms: Long) {
_position = ms;
}
fun getHistory(): List<IPlatformLiveEvent> {
synchronized(_history) {
return _history.toList();
@ -85,13 +111,34 @@ class LiveChatManager {
try {
while(_startCounter == counter) {
var nextInterval = 1000L;
if(_pager is JSVODEventPager && _eventsPosition > _position) {
delay(500);
continue;
}
try {
if(_pager == null || !_pager.hasMorePages())
return@launch;
_pager.nextPage();
val newEvents = _pager.getResults();
val newEvents = if(_pager is JSVODEventPager) {
val requestPosition = _position;
_pager.nextPage(requestPosition.toInt());
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
if(replayResults.size > 0) {
_eventsPosition = replayResults.maxOf { it.time };
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
}
else
_eventsPosition = requestPosition + _pager.nextRequest.coerceAtLeast(800).toLong();
replayResults;
}
else {
_pager.nextPage();
_pager.getResults();
}
if(_pager is JSLiveEventPager)
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
else if(_pager is JSVODEventPager)
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
if(newEvents.size > 0)
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");

View file

@ -20,7 +20,8 @@ data class PlatformClientCapabilities(
val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false,
val hasGetChannelPlaylists: Boolean = false,
val hasGetContentRecommendations: Boolean = false
val hasGetContentRecommendations: Boolean = false,
val hasGetUserHistory: Boolean = false
) {
}

View file

@ -14,14 +14,16 @@ class PlatformClientPool {
private var _poolCounter = 0;
private val _poolName: String?;
private val _privatePool: Boolean;
private val _isolatedInitialization: Boolean
var isDead: Boolean = false
private set;
val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false, isolatedInitialization: Boolean = false) {
_poolName = name;
_privatePool = privatePool;
_isolatedInitialization = isolatedInitialization
if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started");
@ -32,8 +34,10 @@ class PlatformClientPool {
isDead = true;
onDead.emit(parentClient, this);
for(clientPair in _pool) {
clientPair.key.disable();
synchronized(_pool) {
for (clientPair in _pool) {
clientPair.key.disable();
}
}
};
}
@ -53,7 +57,7 @@ class PlatformClientPool {
reserved = _pool.keys.find { !it.isBusy };
if(reserved == null && _pool.size < capacity) {
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
reserved = _parent.getCopy(_privatePool);
reserved = _parent.getCopy(_privatePool, _isolatedInitialization);
reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex);

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable
@ -44,6 +45,7 @@ class PlatformID {
val NONE = PlatformID("Unknown", null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
value.ensureIsBusy();
val contextName = "PlatformID";
return PlatformID(
value.getOrThrow(config, "platform", contextName),

View file

@ -7,13 +7,15 @@ class PlatformMultiClientPool {
private var _isFake = false;
private var _privatePool = false;
private val _isolatedInitialization: Boolean
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false, isolatedInitialization: Boolean = false) {
_name = name;
_maxCap = if(maxCap > 0)
maxCap
else 99;
_privatePool = isPrivatePool;
_isolatedInitialization = isolatedInitialization
}
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
@ -21,7 +23,7 @@ class PlatformMultiClientPool {
return parentClient;
val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool, _isolatedInitialization).apply {
this.onDead.subscribe { _, pool ->
synchronized(_clientPools) {
if(_clientPools[parentClient] == pool)

View file

@ -2,7 +2,11 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -30,6 +34,7 @@ open class PlatformAuthorLink {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
value.ensureIsBusy();
if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value);
@ -42,4 +47,21 @@ open class PlatformAuthorLink {
);
}
}
}
interface IPlatformChannelContent : IPlatformContent {
val thumbnail: String?
val subscribers: Long?
}
open class JSChannelContent : JSContent, IPlatformChannelContent {
override val contentType: ContentType get() = ContentType.CHANNEL
override val thumbnail: String?
override val subscribers: Long?
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Channel";
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
}
}

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -20,6 +21,7 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
companion object {
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
value.ensureIsBusy();
val context = "AuthorMembershipLink"
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
value.getOrThrow(config ,"name", context),

View file

@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.expectV8Variant
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -30,6 +31,7 @@ class ResultCapabilities(
const val TYPE_POSTS = "POSTS";
const val TYPE_MIXED = "MIXED";
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
const val TYPE_SHORTS = "SHORTS";
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
@ -45,6 +47,7 @@ class ResultCapabilities(
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
val contextName = "ResultCapabilities";
value.ensureIsBusy();
return ResultCapabilities(
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
@ -68,6 +71,7 @@ class FilterGroup(
companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
value.ensureIsBusy();
return FilterGroup(
value.getString("name"),
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
@ -89,6 +93,7 @@ class FilterCapability(
companion object {
fun fromV8(obj: V8ValueObject): FilterCapability {
obj.ensureIsBusy();
val value = obj.get("value") as V8Value;
return FilterCapability(
obj.getString("name"),

View file

@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -31,6 +32,7 @@ class Thumbnails {
companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
value.ensureIsBusy();
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) }

View file

@ -0,0 +1,9 @@
package com.futo.platformplayer.api.media.models.article
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
interface IPlatformArticle: IPlatformContent {
val summary: String?;
val thumbnails: Thumbnails?;
}

View file

@ -0,0 +1,12 @@
package com.futo.platformplayer.api.media.models.article
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.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
val segments: List<IJSArticleSegment>;
val rating : IRating;
}

View file

@ -8,10 +8,12 @@ enum class ContentType(val value: Int) {
POST(2),
ARTICLE(3),
PLAYLIST(4),
WEB(7),
URL(9),
NESTED_VIDEO(11),
CHANNEL(60),
LOCKED(70),

View file

@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.contents
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime
interface IPlatformContent {

View file

@ -2,14 +2,17 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
interface IPlatformLiveEvent {
val type : LiveEventType;
var time: Long;
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
obj.ensureIsBusy();
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);

View file

@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -17,16 +18,21 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
val colorName: String?;
val badges: List<String>;
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null) {
override var time: Long = -1;
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null, time: Long = -1) {
this.name = name;
this.message = message;
this.thumbnail = thumbnail;
this.colorName = colorName;
this.badges = badges ?: listOf();
this.time = time;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
obj.ensureIsBusy();
val contextName = "LiveEventComment"
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);
@ -36,7 +42,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
obj.getOrThrow(config, "name", contextName),
obj.getOrThrow(config, "thumbnail", contextName, true),
obj.getOrThrow(config, "message", contextName),
colorName, badges);
colorName, badges,
obj.getOrDefault(config, "time", contextName, -1) ?: -1);
}
}
}

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -20,6 +21,8 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
var expire: Int = 6000;
override var time: Long = -1;
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
this.name = name;
@ -37,6 +40,7 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
obj.ensureIsBusy();
val contextName = "LiveEventDonation"
return LiveEventDonation(
obj.getOrThrow(config, "name", contextName),

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class LiveEventEmojis: IPlatformLiveEvent {
@ -9,15 +10,17 @@ class LiveEventEmojis: IPlatformLiveEvent {
val emojis: HashMap<String, String>;
override var time: Long = -1;
constructor(emojis: HashMap<String, String>) {
this.emojis = emojis;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
obj.ensureIsBusy();
val contextName = "LiveEventEmojis"
return LiveEventEmojis(
obj.getOrThrow(config, "emojis", contextName));
return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
}
}
}

View file

@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
class LiveEventRaid: IPlatformLiveEvent {
@ -10,20 +12,26 @@ class LiveEventRaid: IPlatformLiveEvent {
val targetName: String;
val targetThumbnail: String;
val targetUrl: String;
val isOutgoing: Boolean;
constructor(name: String, url: String, thumbnail: String) {
override var time: Long = -1;
constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
this.targetName = name;
this.targetUrl = url;
this.targetThumbnail = thumbnail;
this.isOutgoing = isOutgoing;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
obj.ensureIsBusy();
val contextName = "LiveEventRaid"
return LiveEventRaid(
obj.getOrThrow(config, "targetName", contextName),
obj.getOrThrow(config, "targetUrl", contextName),
obj.getOrThrow(config, "targetThumbnail", contextName));
obj.getOrThrow(config, "targetThumbnail", contextName),
obj.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
}
}
}

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class LiveEventViewCount: IPlatformLiveEvent {
@ -9,12 +10,15 @@ class LiveEventViewCount: IPlatformLiveEvent {
val viewCount: Int;
override var time: Long = -1;
constructor(viewCount: Int) {
this.viewCount = viewCount;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
obj.ensureIsBusy();
val contextName = "LiveEventViewCount"
return LiveEventViewCount(
obj.getOrThrow(config, "viewCount", contextName));

View file

@ -5,7 +5,8 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
enum class TextType(val value: Int) {
RAW(0),
HTML(1),
MARKUP(2);
MARKUP(2),
CODE(3);
companion object {
fun fromInt(value: Int): TextType

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault
import com.futo.platformplayer.serializers.IRatingSerializer
@ -13,8 +14,12 @@ interface IRating {
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): IRating {
obj?.ensureIsBusy();
return obj.orDefault(default) { fromV8(config, it as V8ValueObject) }
};
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
obj.ensureIsBusy();
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj);

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@ -14,6 +15,7 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
obj.ensureIsBusy();
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
}
}

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@ -13,6 +14,7 @@ class RatingLikes(val likes: Long) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
obj.ensureIsBusy();
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
}
}

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@ -13,6 +14,7 @@ class RatingScaler(val value: Float) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
obj.ensureIsBusy()
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
}
}

View file

@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.downloads.VideoLocal
class LocalVideoMuxedSourceDescriptor(
class DownloadedVideoMuxedSourceDescriptor(
private val video: VideoLocal
) : VideoMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();

View file

@ -13,7 +13,8 @@ class AudioUrlSource(
override val codec: String = "",
override val language: String = Language.UNKNOWN,
override val duration: Long? = null,
override var priority: Boolean = false
override var priority: Boolean = false,
override var original: Boolean = false
) : IAudioUrlSource, IStreamMetaDataSource{
override var streamMetaData: StreamMetaData? = null;
@ -36,7 +37,9 @@ class AudioUrlSource(
source.container,
source.codec,
source.language,
source.duration
source.duration,
source.priority,
source.original
);
ret.streamMetaData = streamData;

View file

@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
override val language: String,
override val duration: Long?,
override val priority: Boolean,
override val original: Boolean,
val url: String
) : IAudioUrlSource {
override fun getAudioUrl(): String {

View file

@ -8,4 +8,5 @@ interface IAudioSource {
val language : String;
val duration : Long?;
val priority: Boolean;
val original: Boolean;
}

View file

@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
override val duration: Long? = null;
override var priority: Boolean = false;
override val original: Boolean = false;
val filePath : String;
val fileSize: Long;
@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
}
companion object {
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
return LocalAudioSource(
source.name,
path,
fileSize,
source.bitrate,
source.container,
overrideContainer ?: source.container,
source.codec,
source.language
);

View file

@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
}
companion object {
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
return LocalVideoSource(
source.name,
path,
@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
source.width,
source.height,
source.duration,
source.container,
overrideContainer ?: source.container,
source.codec,
source.bitrate?:0
);

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import java.time.OffsetDateTime
/**
* A search result representing a video (overview data)
@ -12,5 +13,10 @@ interface IPlatformVideo : IPlatformContent {
val duration: Long;
val viewCount: Long;
val playbackTime: Long;
val playbackDate: OffsetDateTime?;
val isLive : Boolean;
val isShort: Boolean;
}

View file

@ -3,33 +3,39 @@ package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnail
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.polycentric.core.combineHashCodes
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime
@kotlinx.serialization.Serializable
open class SerializedPlatformVideo(
override val contentType: ContentType = ContentType.MEDIA,
override val id: PlatformID,
override val name: String,
override val thumbnails: Thumbnails,
override val thumbnails: Thumbnails = Thumbnails(),
override val author: PlatformAuthorLink,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
@JsonNames("datetime", "dateTime")
override val datetime: OffsetDateTime? = null,
override val url: String,
override val shareUrl: String = "",
override val duration: Long,
override val viewCount: Long,
override val isShort: Boolean = false
) : IPlatformVideo, SerializedPlatformContent {
override val contentType: ContentType = ContentType.MEDIA;
override val isLive: Boolean = false;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override fun toJson() : String {
return Json.encodeToString(this);
}
@ -43,6 +49,7 @@ open class SerializedPlatformVideo(
companion object {
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
return SerializedPlatformVideo(
ContentType.MEDIA,
video.id,
video.name,
video.thumbnails,

View file

@ -13,7 +13,6 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
@ -38,10 +37,15 @@ open class SerializedPlatformVideoDetails(
override val video: ISerializedVideoSourceDescriptor,
override val preview: ISerializedVideoSourceDescriptor?,
override val subtitles: List<SubtitleRawSource> = listOf()
override val subtitles: List<SubtitleRawSource> = listOf(),
override val isShort: Boolean = false
) : IPlatformVideo, IPlatformVideoDetails {
final override val contentType: ContentType get() = ContentType.MEDIA;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override val isLive: Boolean get() = false;
override val dash: IDashManifestSource? get() = null;

View file

@ -54,8 +54,12 @@ class DevJSClient : JSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
}
override fun getCopy(privateCopy: Boolean): JSClient {
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient {
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
}
override fun initialize() {

View file

@ -10,6 +10,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -22,6 +23,7 @@ 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.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
@ -31,6 +33,7 @@ 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.JSChannelContentPager
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
@ -41,6 +44,7 @@ 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.platforms.js.models.JSPlaylistPager
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoPager
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
@ -57,9 +61,13 @@ import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import java.util.Random
import kotlin.Exception
import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction
@ -81,6 +89,8 @@ open class JSClient : IPlatformClient {
private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null;
private var _usedReloadData: String? = null;
protected val _script: String;
private var _initialized: Boolean = false;
@ -96,14 +106,14 @@ open class JSClient : IPlatformClient {
override val icon: ImageVariable;
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
private val _busyLock = Object();
private var _busyCounter = 0;
private var _busyAction = "";
val isBusy: Boolean get() = _busyCounter > 0;
val isBusy: Boolean get() = _plugin.isBusy;
val isBusyAction: String get() {
return _busyAction;
}
val declareOnEnable = HashMap<String, String>();
val settings: HashMap<String, String?> get() = descriptor.settings;
val flags: Array<String>;
@ -116,6 +126,7 @@ open class JSClient : IPlatformClient {
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
val enableInShorts get() = descriptor.appSettings.tabEnabled.enableShorts ?: true
fun getSubscriptionRateLimit(): Int? {
val pluginRateLimit = config.subscriptionRateLimit;
@ -193,8 +204,12 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
}
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
}
fun getUnderlyingPlugin(): V8Plugin {
@ -208,12 +223,31 @@ open class JSClient : IPlatformClient {
return plugin.httpClientOthers[id];
}
fun setReloadData(data: String?) {
if(data == null) {
if(declareOnEnable.containsKey("__reloadData"))
declareOnEnable.remove("__reloadData");
}
else
declareOnEnable.put("__reloadData", data ?: "");
}
fun getReloadData(orLast: Boolean): String? {
if(declareOnEnable.containsKey("__reloadData"))
return declareOnEnable["__reloadData"];
else if(orLast)
return _usedReloadData;
return null;
}
override fun initialize() {
Logger.i(TAG, "Plugin [${config.name}] initializing");
if (_initialized) return
plugin.start();
plugin.execute("plugin.config = ${Json.encodeToString(config)}");
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
descriptor.appSettings.loadDefaults(descriptor.config);
_initialized = true;
@ -238,7 +272,8 @@ open class JSClient : IPlatformClient {
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false,
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
);
try {
@ -253,19 +288,28 @@ open class JSClient : IPlatformClient {
}
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
fun enable() {
fun enable() = isBusyWith("enable") {
if(!_initialized)
initialize();
for(toDeclare in declareOnEnable) {
plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value));
}
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
if(declareOnEnable.containsKey("__reloadData")) {
Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}");
_usedReloadData = declareOnEnable["__reloadData"];
declareOnEnable.remove("__reloadData");
}
_enabled = true;
}
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
fun saveState(): String? {
fun saveState(): String? = isBusyWith("saveState") {
ensureEnabled();
if(!capabilities.hasSaveState)
return null;
return@isBusyWith null;
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
return resp;
return@isBusyWith resp;
}
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
@ -288,6 +332,13 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getHome()"));
}
@JSDocs(2, "source.getShorts()", "Gets the Shorts feed of the platform")
override fun getShorts(): IPager<IPlatformVideo> = isBusyWith("getShorts") {
ensureEnabled()
return@isBusyWith JSVideoPager(config, this,
plugin.executeTyped("source.getShorts()"))
}
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
@JSDocsParameter("query", "Query to complete suggestions for")
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
@ -306,8 +357,10 @@ open class JSClient : IPlatformClient {
return _searchCapabilities!!;
}
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
return _searchCapabilities!!;
return busy {
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
return@busy _searchCapabilities!!;
}
}
catch(ex: Throwable) {
announcePluginUnhandledException("getSearchCapabilities", ex);
@ -335,8 +388,10 @@ open class JSClient : IPlatformClient {
if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!;
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
return _searchChannelContentsCapabilities!!;
return busy {
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
return@busy _searchChannelContentsCapabilities!!;
}
}
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
@JSDocsParameter("channelUrl", "Channel url to search")
@ -361,17 +416,21 @@ open class JSClient : IPlatformClient {
return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
}
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
ensureEnabled();
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
}
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
@JSDocsParameter("url", "A channel url (May not be your platform)")
override fun isChannelUrl(url: String): Boolean {
override fun isChannelUrl(url: String): Boolean = isBusyWith("isChannelUrl") {
try {
return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex);
return false;
return@isBusyWith false;
}
}
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@ -389,9 +448,10 @@ open class JSClient : IPlatformClient {
if (_channelCapabilities != null) {
return _channelCapabilities!!;
}
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return _channelCapabilities!!;
return busy {
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return@busy _channelCapabilities!!;
};
}
catch(ex: Throwable) {
announcePluginUnhandledException("getChannelCapabilities", ex);
@ -502,14 +562,14 @@ open class JSClient : IPlatformClient {
@JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
@JSDocsParameter("url", "A content url (May not be your platform)")
override fun isContentDetailsUrl(url: String): Boolean {
override fun isContentDetailsUrl(url: String): Boolean = isBusyWith("isContentDetailsUrl") {
try {
return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isContentDetailsUrl", ex);
return false;
return@isBusyWith false;
}
}
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@ -541,7 +601,7 @@ open class JSClient : IPlatformClient {
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
if(tracker is V8ValueObject)
return@isBusyWith JSPlaybackTracker(config, tracker);
return@isBusyWith JSPlaybackTracker(this, tracker);
else
return@isBusyWith null;
}
@ -583,7 +643,6 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
}
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
@ -611,17 +670,19 @@ open class JSClient : IPlatformClient {
@JSOptional
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist")
override fun isPlaylistUrl(url: String): Boolean {
override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") {
if (!capabilities.hasGetPlaylist)
return false;
return@isBusyWith false;
try {
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
return@isBusyWith busy {
return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
}
catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex);
return false;
return@isBusyWith false;
}
}
@JSOptional
@ -652,6 +713,13 @@ open class JSClient : IPlatformClient {
.toTypedArray();
}
@JSOptional
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
override fun getUserHistory(): IPager<IPlatformContent> {
ensureEnabled();
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
}
fun validate() {
try {
plugin.start();
@ -723,19 +791,29 @@ open class JSClient : IPlatformClient {
return urls;
}
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
try {
synchronized(_busyLock) {
_busyCounter++;
}
_busyAction = actionName;
return handle();
fun <T> busy(handle: ()->T): T {
return _plugin.busy {
return@busy handle();
}
finally {
_busyAction = "";
synchronized(_busyLock) {
_busyCounter--;
}
fun <T> busyBlockingSuspended(handle: suspend ()->T): T {
return _plugin.busy {
return@busy runBlocking {
return@runBlocking handle();
}
}
}
fun <T> isBusyWith(actionName: String, handle: ()->T): T {
//val busyId = kotlin.random.Random.nextInt(9999);
return busy {
try {
_busyAction = actionName;
return@busy handle();
}
finally {
_busyAction = "";
}
}
}

View file

@ -4,6 +4,7 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual
@ -33,6 +34,7 @@ class SourcePluginConfig(
override val allowEval: Boolean = false,
override val allowUrls: List<String> = listOf(),
override val packages: List<String> = listOf(),
override val packagesOptional: List<String> = listOf(),
val settings: List<Setting> = listOf(),
@ -46,12 +48,14 @@ class SourcePluginConfig(
var subscriptionRateLimit: Int? = null,
var enableInSearch: Boolean = true,
var enableInHome: Boolean = true,
var enableInShorts: Boolean = true,
var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null,
var allowAllHttpHeaderAccess: Boolean = false,
var maxDownloadParallelism: Int = 0,
var reduceFunctionsInLimitedVersion: Boolean = false,
var changelog: HashMap<String, List<String>>? = null
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@ -101,6 +105,10 @@ class SourcePluginConfig(
if(!packages.contains(pack))
return false;
}
for(pack in newConfig.packagesOptional) {
if(!packagesOptional.contains(pack))
return false;
}
//Developer Submit Url should be same or empty
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
return false;
@ -129,7 +137,7 @@ class SourcePluginConfig(
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
if (currentlyInstalledPlugin != null) {
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
list.add(Pair(
"Different Author",
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
@ -162,12 +170,17 @@ class SourcePluginConfig(
}
fun validate(text: String): Boolean {
if(scriptPublicKey.isNullOrEmpty())
throw IllegalStateException("No public key present");
if(scriptSignature.isNullOrEmpty())
throw IllegalStateException("No signature present");
try {
if (scriptPublicKey.isNullOrEmpty())
throw IllegalStateException("No public key present");
if (scriptSignature.isNullOrEmpty())
throw IllegalStateException("No signature present");
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
return false
}
}
fun isUrlAllowed(url: String): Boolean {
@ -178,6 +191,19 @@ class SourcePluginConfig(
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
}
fun getChangelogString(version: String): String?{
if(changelog == null || !changelog!!.containsKey(version))
return null;
val changelog = changelog!![version]!!;
if(changelog.size > 1) {
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
}
else if(changelog.size == 1) {
return "Changelog (${version})\n" + changelog[0].trim();
}
return null;
}
companion object {
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
@ -185,6 +211,8 @@ class SourcePluginConfig(
obj.sourceUrl = sourceUrl;
return obj;
}
private val TAG = "SourcePluginConfig"
}
@kotlinx.serialization.Serializable

View file

@ -5,10 +5,16 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.fields.DropdownFieldOptions
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.FormFieldWarning
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@Serializable
@ -103,12 +109,22 @@ class SourcePluginDescriptor {
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
var enableHome: Boolean? = null;
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
var enableSearch: Boolean? = null;
@FormField(R.string.shorts, FieldForm.TOGGLE, R.string.show_content_in_shorts_tab, 3)
var enableShorts: Boolean? = null;
}
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
@FormField(R.string.sync, "group", R.string.sync_desc, 3,"sync")
var sync = Sync();
@Serializable
class Sync {
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1,"syncHistory")
var enableHistorySync: Boolean? = null;
}
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
var rateLimit = RateLimit();
@Serializable
class RateLimit {
@ -143,6 +159,8 @@ class SourcePluginDescriptor {
tabEnabled.enableHome = config.enableInHome
if(tabEnabled.enableSearch == null)
tabEnabled.enableSearch = config.enableInSearch
if(tabEnabled.enableShorts == null)
tabEnabled.enableShorts = config.enableInShorts
}
}

View file

@ -67,6 +67,25 @@ class JSHttpClient : ManagedHttpClient {
}
fun resetAuthCookies() {
_currentCookieMap.clear();
if(!_auth?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _auth!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
if(!_captcha?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _captcha!!.cookieMap!!) {
if(_currentCookieMap.containsKey(domainCookies.key))
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
else
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
}
}
fun clearOtherCookies() {
_otherCookieMap.clear();
}
override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
@ -127,7 +146,7 @@ class JSHttpClient : ManagedHttpClient {
}
if(doApplyCookies) {
if (_currentCookieMap.isNotEmpty()) {
if (_currentCookieMap.isNotEmpty() || _otherCookieMap.isNotEmpty()) {
val cookiesToApply = hashMapOf<String, String>();
synchronized(_currentCookieMap) {
for(cookie in _currentCookieMap
@ -135,6 +154,12 @@ class JSHttpClient : ManagedHttpClient {
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
};
synchronized(_otherCookieMap) {
for(cookie in _otherCookieMap
.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("; ");

View file

@ -1,10 +1,12 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.JSChannelContent
import com.futo.platformplayer.api.media.models.contents.ContentType
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.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -12,6 +14,7 @@ interface IJSContent: IPlatformContent {
companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
obj.ensureIsBusy();
val config = plugin.config;
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
@ -26,6 +29,9 @@ interface IJSContent: IPlatformContent {
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
ContentType.PLAYLIST -> JSPlaylist(config, obj);
ContentType.LOCKED -> JSLockedContent(config, obj);
ContentType.CHANNEL -> JSChannelContent(config, obj);
ContentType.ARTICLE -> JSArticle(config, obj);
ContentType.WEB -> JSWeb(config, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}

View file

@ -6,17 +6,20 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
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.ensureIsBusy
import com.futo.platformplayer.getOrThrow
interface IJSContentDetails: IPlatformContent {
companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
obj.ensureIsBusy();
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
ContentType.WEB -> JSWebDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}

View file

@ -0,0 +1,39 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
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.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
final override val contentType: ContentType get() = ContentType.ARTICLE;
override val summary: String;
override val thumbnails: Thumbnails?;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformArticle";
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
}
}

View file

@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@ -19,22 +21,23 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.ARTICLE;
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
val rating: IRating;
override val rating: IRating;
val summary: String;
val thumbnails: Thumbnails?;
val segments: List<IJSArticleSegment>;
override val summary: String;
override val thumbnails: Thumbnails?;
override val segments: List<IJSArticleSegment>;
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformPost";
val contextName = "PlatformArticle";
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
summary = _content.getOrThrow(client.config, "summary", contextName);
@ -83,12 +86,12 @@ open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}
@ -99,6 +102,7 @@ open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.HEADER -> JSHeaderSegment(client, obj);
SegmentType.NESTED -> JSNestedSegment(client, obj);
else -> null;
}
@ -110,6 +114,7 @@ enum class SegmentType(val value: Int) {
UNKNOWN(0),
TEXT(1),
IMAGES(2),
HEADER(3),
NESTED(9);
@ -150,6 +155,17 @@ class JSImagesSegment: IJSArticleSegment {
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
}
}
class JSHeaderSegment: IJSArticleSegment {
override val type = SegmentType.HEADER;
val content: String;
val level: Int;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSHeaderSegment";
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
level = obj.getOrDefault(client.config, "level", contextName, 1) ?: 1;
}
}
class JSNestedSegment: IJSArticleSegment {
override val type = SegmentType.NESTED;
val nested: IPlatformContent;

View file

@ -5,7 +5,6 @@ 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.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {

View file

@ -12,6 +12,7 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import java.time.LocalDateTime
import java.time.OffsetDateTime
@ -60,7 +61,7 @@ class JSComment : IPlatformComment {
if(!_hasGetReplies)
return null;
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return JSCommentPager(_config!!, plugin, obj);
}

View file

@ -49,8 +49,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
else
author = PlatformAuthorLink.UNKNOWN;
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
if(datetimeInt == 0.toLong())
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
if(datetimeInt == null || datetimeInt == 0.toLong())
datetime = null;
else
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.JSChannelContent
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
@ -15,4 +16,14 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
override fun convertResult(obj: V8ValueObject): IPlatformContent {
return IJSContent.fromV8(plugin, obj);
}
}
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
override val sourceConfig: SourcePluginConfig get() = config;
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformContent {
return JSChannelContent(config, obj);
}
}

View file

@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}
override fun nextPage() {
override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") {
super.nextPage();
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}

View file

@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.warnIfMainThread
abstract class JSPager<T> : IPager<T> {
@ -18,8 +19,8 @@ abstract class JSPager<T> : IPager<T> {
protected var pager: V8ValueObject;
private var _lastResults: List<T>? = null;
private var _resultChanged: Boolean = true;
private var _hasMorePages: Boolean = false;
protected var _resultChanged: Boolean = true;
protected var _hasMorePages: Boolean = false;
//private var _morePagesWasFalse: Boolean = false;
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
@ -29,7 +30,9 @@ abstract class JSPager<T> : IPager<T> {
this.pager = pager;
this.config = config;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
plugin.busy {
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}
getResults();
}
@ -38,17 +41,20 @@ abstract class JSPager<T> : IPager<T> {
}
override fun hasMorePages(): Boolean {
return _hasMorePages;
return _hasMorePages && !pager.isClosed;
}
override fun nextPage() {
warnIfMainThread("JSPager.nextPage");
pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invoke("nextPage", arrayOf<Any>());
};
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
val pluginV8 = plugin.getUnderlyingPlugin();
pluginV8.busy {
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invokeV8("nextPage", arrayOf<Any>());
};
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
}
/*
try {
}
@ -70,15 +76,18 @@ abstract class JSPager<T> : IPager<T> {
return previousResults;
warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
throw IllegalStateException("Runtime closed");
val newResults = items.toArray()
.map { convertResult(it as V8ValueObject) }
.toList();
_lastResults = newResults;
_resultChanged = false;
return newResults;
return plugin.getUnderlyingPlugin().busy {
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if (items.v8Runtime.isDead || items.v8Runtime.isClosed)
throw IllegalStateException("Runtime closed");
val newResults = items.toArray()
.map { convertResult(it as V8ValueObject) }
.toList();
_lastResults = newResults;
_resultChanged = false;
return@busy newResults;
}
}
abstract fun convertResult(obj: V8ValueObject): T;

View file

@ -2,37 +2,51 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.warnIfMainThread
class JSPlaybackTracker: IPlaybackTracker {
private val _config: IV8PluginConfig;
private val _obj: V8ValueObject;
private lateinit var _client: JSClient;
private lateinit var _config: IV8PluginConfig;
private lateinit var _obj: V8ValueObject;
private var _hasCalledInit: Boolean = false;
private val _hasInit: Boolean;
private var _hasInit: Boolean = false;
private var _lastRequest: Long = Long.MIN_VALUE;
private val _hasOnConcluded: Boolean;
private var _hasOnConcluded: Boolean = false;
override var nextRequest: Int = 1000
private set;
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
constructor(client: JSClient, obj: V8ValueObject) {
warnIfMainThread("JSPlaybackTracker.constructor");
if(!obj.has("onProgress"))
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
if(!obj.has("nextRequest"))
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
_hasOnConcluded = obj.has("onConcluded");
this._config = config;
this._obj = obj;
this._hasInit = obj.has("onInit");
client.busy {
if (!obj.has("onProgress"))
throw ScriptImplementationException(
client.config,
"Missing onProgress on PlaybackTracker"
);
if (!obj.has("nextRequest"))
throw ScriptImplementationException(
client.config,
"Missing nextRequest on PlaybackTracker"
);
_hasOnConcluded = obj.has("onConcluded");
this._client = client;
this._config = client.config;
this._obj = obj;
this._hasInit = obj.has("onInit");
}
}
override fun onInit(seconds: Double) {
@ -40,12 +54,15 @@ class JSPlaybackTracker: IPlaybackTracker {
synchronized(_obj) {
if(_hasCalledInit)
return;
if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeVoid("onInit", seconds);
_client.busy {
if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeV8Void("onInit", seconds);
}
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true;
}
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true;
}
}
@ -55,10 +72,12 @@ class JSPlaybackTracker: IPlaybackTracker {
if(!_hasCalledInit && _hasInit)
onInit(seconds);
else {
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
_obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying);
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_lastRequest = System.currentTimeMillis();
_client.busy {
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
_obj.invokeV8Void("onProgress", Math.floor(seconds), isPlaying);
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_lastRequest = System.currentTimeMillis();
}
}
}
}
@ -67,7 +86,9 @@ class JSPlaybackTracker: IPlaybackTracker {
if(_hasOnConcluded) {
synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded");
_obj.invokeVoid("onConcluded", -1);
_client.busy {
_obj.invokeV8Void("onConcluded", -1);
}
}
}
}

View file

@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
@ -68,12 +69,12 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}

View file

@ -14,6 +14,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.Serializable
@ -46,52 +48,55 @@ class JSRequestExecutor {
if (_executor.isClosed)
throw IllegalStateException("Executor object is closed");
val result = if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers, method, body);
} as V8Value;
}
return _plugin.getUnderlyingPlugin().busy {
val result = if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeV8("executeRequest", url, headers, method, body);
} as V8Value;
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers, method, body);
_executor.invokeV8("executeRequest", url, headers, method, body);
} as V8Value;
try {
if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value);
return base64Result;
}
if(result is V8ValueTypedArray) {
val buffer = result.buffer;
val byteBuffer = buffer.byteBuffer;
val bytesResult = ByteArray(result.byteLength);
byteBuffer.get(bytesResult, 0, result.byteLength);
buffer.close();
return bytesResult;
}
if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
when(type) {
//TODO: Buffer type?
try {
if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value);
return@busy base64Result;
}
if(result is V8ValueTypedArray) {
val buffer = result.buffer;
val byteBuffer = buffer.byteBuffer;
val bytesResult = ByteArray(result.byteLength);
byteBuffer.get(bytesResult, 0, result.byteLength);
buffer.close();
return@busy bytesResult;
}
if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
when(type) {
//TODO: Buffer type?
}
}
if(result is V8ValueUndefined) {
if(_plugin is DevJSClient)
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
}
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
}
if(result is V8ValueUndefined) {
if(_plugin is DevJSClient)
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
finally {
result.close();
}
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
}
finally {
result.close();
}
}
@ -99,24 +104,25 @@ class JSRequestExecutor {
open fun cleanup() {
if (!hasCleanup || _executor.isClosed)
return;
if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
};
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
};
_plugin.busy {
if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeV8("cleanup", null);
};
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeV8("cleanup", null);
};
}
}
protected fun finalize() {

View file

@ -11,12 +11,14 @@ import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient;
private val _config: IV8PluginConfig;
private var _modifier: V8ValueObject;
override var allowByteSkip: Boolean;
override var allowByteSkip: Boolean = false;
constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin;
@ -24,10 +26,13 @@ class JSRequestModifier: IRequestModifier {
this._config = plugin.config;
val config = plugin.config;
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
plugin.busy {
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
@ -35,13 +40,15 @@ class JSRequestModifier: IRequestModifier {
return Request(url, headers);
}
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers);
} as V8ValueObject;
return _plugin.busy {
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invokeV8("modifyRequest", url, headers);
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers);
result.close();
return req;
val req = JSRequest(_plugin, result, url, headers);
result.close();
return@busy req;
}
}

View file

@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -35,8 +37,11 @@ class JSSubtitleSource : ISubtitleSource {
override fun getSubtitles(): String {
if(!hasFetch)
throw IllegalStateException("This subtitle doesn't support getSubtitles..");
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>());
return v8String.value;
return _obj.getSourcePlugin()?.busy {
val v8String = _obj.invokeV8<V8ValueString>("getSubtitles", arrayOf<Any>());
return@busy v8String.value;
} ?: "";
}
override suspend fun getSubtitlesURI(): Uri? {

View file

@ -0,0 +1,44 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
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.structures.IPlatformLiveEventPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.warnIfMainThread
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class JSVODEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
override var nextRequest: Int;
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}
fun nextPage(ms: Int) = plugin.isBusyWith("JSLiveEventPager.nextPage") {
warnIfMainThread("VODEventPager.nextPage");
val pluginV8 = plugin.getUnderlyingPlugin();
pluginV8.busy {
val newPager: V8Value = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage(...)") {
pager.invokeV8<V8Value>("nextPage", ms);
};
if(newPager is V8ValueObject)
pager = newPager;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
}
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}
override fun nextPage() = nextPage(0);
override fun convertResult(obj: V8ValueObject): IPlatformLiveEvent {
return IPlatformLiveEvent.fromV8(config, obj, "LiveEventPager");
}
}

View file

@ -6,7 +6,12 @@ import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
final override val contentType: ContentType get() = ContentType.MEDIA;
@ -16,7 +21,12 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
final override val duration: Long;
final override val viewCount: Long;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
final override val isLive: Boolean;
final override val isShort: Boolean;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformVideo";
@ -26,5 +36,12 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
viewCount = _content.getOrThrow(config, "viewCount", contextName);
isLive = _content.getOrThrow(config, "isLive", contextName);
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
playbackTime = _content.getOrDefault<Long>(config, "playbackTime", contextName, -1)?.toLong() ?: -1;
val playbackDateInt = _content.getOrDefault<Int>(config, "playbackDate", contextName, null)?.toLong();
if(playbackDateInt == null || playbackDateInt == 0.toLong())
playbackDate = null;
else
playbackDate = OffsetDateTime.of(LocalDateTime.ofEpochSecond(playbackDateInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
}
}

View file

@ -7,6 +7,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
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.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
@ -24,12 +25,17 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private val _plugin: JSClient;
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
private val _hasGetPlaybackTracker: Boolean;
private val _hasGetVODEvents: Boolean;
//Details
override val description : String;
@ -45,9 +51,9 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
override val subtitles: List<ISubtitleSource>;
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
val contextName = "VideoDetails";
_plugin = plugin;
val config = plugin.config;
description = _content.getOrThrow(config, "description", contextName);
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
@ -69,6 +75,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
_hasGetComments = _content.has("getComments");
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
_hasGetVODEvents = _content.has("getVODEvents");
}
override fun getPlaybackTracker(): IPlaybackTracker? {
@ -82,14 +89,16 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return getPlaybackTrackerJS();
}
private fun getPlaybackTrackerJS(): IPlaybackTracker? {
return V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>())
?: return@catchScriptErrors null;
if(tracker is V8ValueObject)
return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker);
else
return@catchScriptErrors null;
};
return _plugin.busy {
V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
val tracker = _content.invokeV8<V8Value>("getPlaybackTracker", arrayOf<Any>())
?: return@catchScriptErrors null;
if(tracker is V8ValueObject)
return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
else
return@catchScriptErrors null;
}
}
}
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
@ -106,8 +115,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
return _plugin.busy {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return@busy JSContentPager(_pluginConfig, client, contentPager);
}
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
@ -123,10 +134,23 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
}
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return null;
return _plugin.busy {
val commentPager = _content.invokeV8<V8Value>("getComments", arrayOf<Any>());
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return@busy null;
return JSCommentPager(_pluginConfig, client, commentPager);
return@busy JSCommentPager(_pluginConfig, client, commentPager);
}
}
fun hasVODEvents(): Boolean{
return _hasGetVODEvents;
}
fun getVODEvents(url: String): IPager<IPlatformLiveEvent>? = _plugin.busy {
if(!_hasGetVODEvents)
return@busy null;
return@busy JSVODEventPager(_plugin.config, _plugin,
_content.invokeV8<V8ValueObject>("getVODEvents", arrayOf<Any>()));
}
}

View file

@ -0,0 +1,31 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
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.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSWeb : JSContent, IPluginSourced {
final override val contentType: ContentType get() = ContentType.WEB;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformWeb";
}
}

View file

@ -0,0 +1,41 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
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.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSWebDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.WEB;
val html: String?;
//TODO: Options?
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformWeb";
html = obj.getOrDefault(client.config, "html", contextName, null);
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
}

View file

@ -21,6 +21,8 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
override var priority: Boolean = false;
override var original: Boolean = false;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
val contextName = "AudioUrlSource";
val config = plugin.config;
@ -35,6 +37,7 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
}
override fun getAudioUrl() : String {

View file

@ -6,6 +6,8 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
override val licenseUri: String
@ -25,7 +27,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)

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