544 Commits

Author SHA1 Message Date
eddyizm
bd872fc23d chore: bumped version and updated changelog 2026-01-10 07:52:19 -08:00
eddyizm
64a1966ad8 Bug/instant mix issues (#344)
* fix: song bottom sheet changed to livedata and fixed issue

* fix: refactor bottom sheet instant mix calls to use livedata.
2026-01-09 18:44:59 -08:00
eddyizm
5ef5731fe3 feat: Ability to toggle visibility of artist biography (#338) 2026-01-08 21:25:54 -08:00
kmarius
c5cece8477 Hide biography section when no info is available 2026-01-07 18:14:53 +01:00
kmarius
bae9221070 feat: Ability to toggle artist biography 2026-01-07 18:14:51 +01:00
eddyizm
c0dbe01bf9 chore: updated read me and pending release info 2026-01-06 21:05:24 -08:00
eddyizm
5f550b0df4 chore(i18n): Update Polish translation (#339) 2026-01-05 07:33:48 -08:00
skajmer
6100c3e7f1 Add #330 2026-01-05 14:59:19 +01:00
skajmer
f01ca9fed0 Merge branch 'eddyizm:development' into development 2026-01-05 14:56:33 +01:00
eddyizm
d232ebfa6f feat(i18n): add missing keys, update Chinese translation and alphabetize (#332) 2026-01-04 15:41:48 -08:00
eddyizm
53ca88989f Merge branch 'development' into improve/update-zh 2026-01-04 15:41:31 -08:00
eddyizm
a82cf70433 Bug/instant mix issue (#330) 2026-01-04 11:57:20 -08:00
eddyizm
89aa18b5f0 chore: Clarify Android Auto enablement (#336) 2026-01-04 11:56:56 -08:00
eddyizm
431014adc4 fix: updated song bottom sheet to match album/artist bottom sheets 2026-01-04 11:31:53 -08:00
eddyizm
6110a9c8e7 fix: added a timeout for the callbacks to dismiss dialog and notify the user 2026-01-04 10:05:16 -08:00
eddyizm
993374e56c fix: adde a scheduled delay to allow callbacks to succeed 2026-01-04 09:27:53 -08:00
eddyizm
a2801f3168 fix: reduced debounce, added toast 2026-01-04 07:53:07 -08:00
eddyizm
99c31f4318 wip: bumps debounce time 2026-01-03 08:18:29 -08:00
eddyizm
05785979e3 fix: cleans up duplicates 2026-01-03 08:17:53 -08:00
Age Bosma
586a1a160e Clarify Android Auto enablement 2026-01-03 15:53:37 +01:00
eddyizm
d04ed8d430 fix: address duplicate track bug, wrong order in queue, and updated album instant mix 2026-01-01 11:50:48 -08:00
eddyizm
193447d07e fix: used set to address duplicates and removed toast that was firing to early 2025-12-31 08:20:03 -08:00
hongwei
1725b0de2e feat(i18n): add missing keys, update Chinese translation and alphabetize 2025-12-31 14:06:57 +08:00
eddyizm
a2401302ed wip: beta build 2025-12-30 21:46:07 -08:00
eddyizm
f39891dd2c wip: added logging to media manager to track down bug in bottom sheet dialogs 2025-12-29 16:37:20 -08:00
eddyizm
8c5390bfef wip: more instant mix refactor to be able to accumulate tracks 2025-12-29 16:36:42 -08:00
eddyizm
10673a49d4 chore: bump version, bringing in dev changes to test 2025-12-28 08:20:58 -08:00
eddyizm
3ce34fb874 Merge branch 'development' into bug/instant-mix-issue 2025-12-28 08:19:38 -08:00
eddyizm
5c94e9122c chore: bumped version 2025-12-28 08:12:48 -08:00
eddyizm
8140e80d61 wip: artist logic squared away, seems to be working as expected, mostly. still need more testing 2025-12-27 19:05:14 -08:00
eddyizm
c1b2ec09a4 wip: radio working from artist page 2025-12-27 17:52:29 -08:00
eddyizm
3b3f55c5de wip: point artist repo instant mix to song repo 2025-12-27 17:50:53 -08:00
eddyizm
17020e5192 wip: album tracks working, album bottom sheet only pulling in the album, not quite right 2025-12-27 17:48:06 -08:00
eddyizm
f22aea7b1d wip: changed seedtype constant to camelcase, updated references 2025-12-27 17:46:16 -08:00
eddyizm
844b57054b wip: updated album repo for song instant mix type update 2025-12-27 12:31:01 -08:00
eddyizm
8de9aff1f6 wip: refactor song repo instant mix to take in a type 2025-12-27 12:27:18 -08:00
eddyizm
f59f572e5c wip: added queue type to for instant mix calls 2025-12-27 11:04:43 -08:00
skajmer
da2221540e Update strings.xml 2025-12-27 19:23:51 +01:00
skajmer
9fa29c183a Add #328 2025-12-27 19:21:57 +01:00
eddyizm
d034171d92 chore: formatting 2025-12-27 08:17:16 -08:00
eddyizm
3a30b3d379 feat: finishing up album bottom sheet dialog updates for instant mix refactor 2025-12-26 22:12:53 -08:00
eddyizm
2624f396e5 wip: refactor album repo to use the song repo instant mix 2025-12-26 22:08:07 -08:00
eddyizm
8ae32a3a22 fix: give user feedback when trying to add podcast/radio on unsupport… (#328) 2025-12-26 19:20:29 -08:00
eddyizm
3c1975f6bf wip: initial refactor of instant mix in to be used everywhere else 2025-12-26 17:03:41 -08:00
eddyizm
43a96faca4 fix: give user feedback when trying to add podcast/radio on unsupported back servers 2025-12-25 14:03:30 -08:00
eddyizm
bbd6d0864c chore: added airsonic to docs, comments in home tab xml 2025-12-24 07:16:16 -08:00
eddyizm
ccea7674bd chore: bumped version, added fastlane metadata 2025-12-22 19:14:29 -08:00
eddyizm
7f332c26ad chore: changeup bump for version 2025-12-22 19:10:10 -08:00
eddyizm
206a7f38ca chore: updated changelog 2025-12-22 18:39:40 -08:00
eddyizm
16e0a5e12e feat: added regular playlist to home view (#322) 2025-12-22 18:36:56 -08:00
eddyizm
c6896939e2 fix: serialized corrected mapping for playlist cover art to appear 2025-12-22 18:30:49 -08:00
eddyizm
526253723b feat: added regular playlist to home view 2025-12-22 11:04:25 -08:00
eddyizm
9350a9cc2e chore: updating pending release info 2025-12-21 08:20:50 -08:00
eddyizm
e2ec2e4602 Update description_empty_title in French and Spanish (#315) 2025-12-20 10:06:27 -08:00
eddyizm
bca2e8fcae Merge branch 'development' into main 2025-12-20 10:06:03 -08:00
pochopsp
43674ea1f9 Update description_empty_title in French and Spanish 2025-12-20 18:40:55 +01:00
eddyizm
373a1f87a1 Update description_empty_title in Italian (#314) 2025-12-20 07:56:43 -08:00
eddyizm
e14a595fba Merge branch 'development' into patch-1 2025-12-20 07:56:19 -08:00
pochopsp
727e137008 Update description_empty_title in Italian 2025-12-20 12:07:00 +01:00
eddyizm
883d853129 fix: checks preference and writes files externally, updates the ui (#312) 2025-12-17 22:28:20 -08:00
eddyizm
0d329aff64 fix: checks preferecen and writes files externally, updates the ui 2025-12-17 22:27:00 -08:00
eddyizm
94cb6fa279 chore(i18n): Update Polish translation (#310) 2025-12-17 21:25:14 -08:00
eddyizm
257d80ecac Merge branch 'development' into development 2025-12-17 21:25:02 -08:00
eddyizm
d0f77fe0fc Update description_empty_title in English and Polish (#307)
resolves #306
2025-12-17 21:23:11 -08:00
skajmer
e95b504dbb Merge branch 'eddyizm:development' into development 2025-12-17 12:11:19 +01:00
Tymon Flower
0b68799507 Update description_empty_title in English and Polish 2025-12-15 18:25:28 +01:00
eddyizm
9167be2cf2 chore: added new version details 2025-12-12 20:52:05 -08:00
eddyizm
d426c08cdd chore: version bump 2025-12-12 20:45:42 -08:00
eddyizm
972c32b9d8 chore: updated change log, fastlane for pending release 2025-12-12 20:14:10 -08:00
eddyizm
a279e20a49 feat: add heart to artist/album pages, fixed artist cover art failing (#303) 2025-12-12 07:15:11 -08:00
eddyizm
fe60fea928 feat: add heart to artist/album pages, fixed artist cover art failing 2025-12-11 22:07:44 -08:00
skajmer
c6df43da9c left some english in by accident 2025-12-10 22:01:19 +01:00
skajmer
475ed3e7c8 Add #300 2025-12-10 21:59:32 +01:00
skajmer
fb4c762655 Merge branch 'eddyizm:development' into development 2025-12-10 21:56:49 +01:00
eddyizm
a110faabe3 feat: integrate sort recent searches chronologically (#300) 2025-12-08 20:43:15 -08:00
j4mm3ris
df2bf43492 Default the search sort setting to former sorting behavior. 2025-12-07 21:39:10 +02:00
j4mm3ris
b46fea6890 Fix indentation according to previous versions 2025-12-07 21:30:21 +02:00
skajmer
213a0d5293 Add #298 2025-12-07 20:18:40 +01:00
eddyizm
08b6379601 chore: updated readme 2025-12-07 10:48:58 -08:00
eddyizm
3fbadc2521 fix: handle empty albums and null mappings (#301) 2025-12-07 10:05:30 -08:00
eddyizm
9e78caeda4 fix: updates to starred syncing to user defined directory (#298) 2025-12-07 10:05:13 -08:00
eddyizm
e072a49288 fix: handle empty albums and null mappings 2025-12-07 10:04:05 -08:00
j4mm3ris
b89e18eebf feat: integrate sort recent searches chronologically 2025-12-07 13:24:03 +02:00
eddyizm
63607794d6 fix: updates to starred syncing to user defined directory 2025-12-02 21:46:04 -08:00
eddyizm
37842fd897 chore: bumped verison 2025-11-30 17:51:22 -08:00
eddyizm
a1397a224b chore: adding pending release and fastlane updates 2025-11-30 16:43:12 -08:00
eddyizm
804d6af6c3 chore(i18n): Update Polish translation (#291) 2025-11-30 13:49:15 -08:00
skajmer
e315169005 Add #288 2025-11-30 19:57:56 +01:00
skajmer
ea76afee09 Merge branch 'eddyizm:development' into development 2025-11-30 19:52:39 +01:00
eddyizm
45dda3af9b Feat/playerqueue fab (#288) 2025-11-30 10:25:11 -08:00
eddyizm
3d70b51244 fix: updated order of buttons 2025-11-30 09:10:02 -08:00
eddyizm
22f196c8c0 fix: refactor start queue to put the db writing in the background (#287) 2025-11-28 10:00:35 -08:00
eddyizm
540aa9ba73 feat: implemented download queue fab 2025-11-28 09:57:29 -08:00
eddyizm
1ff0b83a19 feat: implemented load queue, adding logging 2025-11-27 13:28:07 -08:00
eddyizm
27f5a47cc9 feat: save q to playlist, removed save queue button, added style to fab. 2025-11-27 08:04:40 -08:00
eddyizm
732b6ad09d fix: moved existing functionality to fab buttons, removed queue text/button from top 2025-11-25 15:48:48 -08:00
eddyizm
0df7346a14 Merge branch 'development' into feat/playerqueue-FAB 2025-11-24 20:52:20 -08:00
eddyizm
786697109d chore: bringing in media service refactor for more testing (#286) 2025-11-24 20:49:48 -08:00
eddyizm
1bfadb0669 fix: refactor start queue to put the db writing in the background 2025-11-24 20:46:46 -08:00
eddyizm
79dc1cc93b chore: bringing in media service refactor for more testing 2025-11-24 13:11:29 -08:00
eddyizm
38fb2c69f1 wip: added fab, need to implement actions 2025-11-24 11:36:56 -08:00
skajmer
b34f827bc0 Add #276 2025-11-23 20:29:20 +01:00
eddyizm
97d1b408e1 chore: bumped version, updated change logs 2025-11-23 09:58:04 -08:00
eddyizm
a5065578ca Fix/start queue blocking UI (#283) 2025-11-23 09:36:31 -08:00
eddyizm
aac5c6067d Merge branch 'library-play' into fix/start-queue-blocking-ui 2025-11-23 09:34:39 -08:00
eddyizm
cfd7cf314b feat: add play functionality to library folder/index items (#276) 2025-11-23 09:33:44 -08:00
eddyizm
c4b73f6014 Merge branch 'development' into main 2025-11-23 09:33:26 -08:00
eddyizm
35d377ce31 Revert "refactor MediaService" (#282) 2025-11-23 09:30:34 -08:00
eddyizm
5e330ac451 Revert "refactor MediaService"
This reverts commit 7aa325f914.
2025-11-23 09:18:32 -08:00
eddyizm
8188ef169c fix: put queue into background thread 2025-11-23 09:09:20 -08:00
eddyizm
3496918ce6 Merge branch 'development' into library-play 2025-11-22 14:30:20 -08:00
eddyizm
c72f368f6a Add Obtainium badge to README (#280) 2025-11-22 11:52:04 -08:00
Mikael Dúi Bolinder
eb089847e0 Add Obtainium badge to README 2025-11-22 21:29:04 +02:00
eddyizm
8aaa6b207e chore: added reproducible build badge 2025-11-22 09:30:01 -08:00
eddyizm
72d560e4eb chore: updated changelog, bumped version for release 2025-11-22 09:22:13 -08:00
eddyizm
31219ea754 fix: it strings needed an single quote escape 2025-11-22 09:07:49 -08:00
eddyizm
52c411ead0 chore(i18n): Update Italian translation (#278) 2025-11-22 08:36:21 -08:00
eddyizm
0edbd15d47 chore(i18n): Update Spanish translation (#272) 2025-11-22 08:34:17 -08:00
eddyizm
342241963a Refactor MediaService (#267) 2025-11-22 08:33:07 -08:00
bunz
f5b381eb35 chore(i18n): Update Italian translation 2025-11-21 13:36:44 +01:00
Ante Budimir
be33401b6f feat: add play functionality to library folder/index items
- add play button to inner folders in library
- implement recursive song collection from folders and subfolders
- filter out video files, play only audio tracks
- add user feedback with toast notifications
2025-11-20 19:13:46 +02:00
eddyizm
c415db0cc5 chore: pending release update 2025-11-16 09:35:53 -08:00
eddyizm
35576c3d6f feat: Add Catalan i18n (#268) 2025-11-16 09:23:06 -08:00
Jaime García
a11fbfa829 chore(i18n): Update Spanish translation 2025-11-16 17:50:45 +01:00
Jaime García
26d1b144e4 chore(i18n): Update Spanish translation 2025-11-16 17:48:51 +01:00
pca006132
16b63bf13c Merge branch 'development' into refactor-mediaservice 2025-11-16 20:33:48 +08:00
Marc Riera
52434f3aa9 feat: Add Catalan i18n 2025-11-16 10:36:14 +01:00
pca006132
7aa325f914 refactor MediaService 2025-11-16 17:22:22 +08:00
pca006132
5a8a631449 fix shuffle 2025-11-16 14:19:58 +08:00
eddyizm
e6bbd7b2bf Fix player queue soft-lock (#266) 2025-11-15 22:11:37 -08:00
observer
3721484dff Fix player queue soft-lock 2025-11-16 00:13:36 +00:00
eddyizm
6698052ba5 chore: updated change log 2025-11-15 10:34:38 -08:00
eddyizm
f6f24acfdf chore: bumped version 2025-11-15 09:31:52 -08:00
eddyizm
ca8bcba0d7 fix: add podcast channel visible when empty podcasts (#260) 2025-11-14 21:55:13 -08:00
eddyizm
9a36f8541f chore: added tested backends to docs as well as podcast/radio feed screenshots 2025-11-14 21:47:46 -08:00
eddyizm
0026dc287f Update Polish translation (#257) 2025-11-14 06:16:30 -08:00
skajmer
c65077172d Update strings.xml 2025-11-14 13:38:19 +01:00
skajmer
6e6c261f35 Update strings.xml 2025-11-14 13:35:05 +01:00
skajmer
130cbbd7dd Missing strings
sort albums by count stuff I missed
2025-11-14 13:28:21 +01:00
eddyizm
63668f5a8c fix: made sure the empty graphic was there when list was empty 2025-11-13 21:37:48 -08:00
eddyizm
193b551773 fix: home radio add station missing from view 2025-11-13 19:07:45 -08:00
eddyizm
76a0e12222 fix: add podcast channel visible when empty podcasts 2025-11-13 16:07:10 -08:00
skajmer
887d8c85ee Accidental typo 2025-11-13 10:38:03 +01:00
skajmer
6a90f06084 Added #253 2025-11-13 10:34:52 +01:00
skajmer
f7a21cbb52 Merge branch 'eddyizm:development' into development 2025-11-13 10:23:43 +01:00
eddyizm
3c6c240b9d chore: more pending release updates 2025-11-12 21:53:35 -08:00
eddyizm
2553c06a9f Fixed crash when viewing share (#255) 2025-11-12 07:01:43 -08:00
eddyizm
62a10d142e fix:github release check (#253) 2025-11-12 06:59:05 -08:00
drakeerv
955dc1b015 Fixed crash when viewing share 2025-11-12 09:45:07 -05:00
eddyizm
6124ec66f3 feat: added setting to disable github check and completely disable/hide when using the degoogled version 2025-11-11 18:06:01 -08:00
eddyizm
2c6287405e fix:github release check 2025-11-11 12:00:05 -08:00
eddyizm
0be309fb22 chore: updated pending release notes 2025-11-11 11:12:54 -08:00
eddyizm
a79543569d fix: disallow duplicate songs in queue (#252) 2025-11-11 11:08:26 -08:00
eddyizm
e3d7120193 chore: Update russian lang strings.xml (#249) 2025-11-11 11:08:02 -08:00
eddyizm
80a3a54476 Merge branch 'development' into patch-1 2025-11-11 11:05:25 -08:00
eddyizm
6448cc598d fix: disallow duplicate songs in queue 2025-11-10 10:53:16 -08:00
skajmer
c4bd30d512 Merge branch 'eddyizm:development' into development 2025-11-10 11:41:33 +01:00
eddyizm
b2e3596d87 chore: updated change log 2025-11-09 17:07:07 -08:00
eddyizm
ccfe74a6ea chore: bump version for release 2025-11-09 16:49:17 -08:00
eddyizm
f4ffdc985e chore: updating pending release and typo in settings 2025-11-09 16:48:16 -08:00
eddyizm
33981f9885 Fix shuffling genres only queuing 25 songs (#246) 2025-11-09 16:41:17 -08:00
observer
1bc93cce0e use overloads instead of new methods 2025-11-09 22:01:33 +00:00
eddyizm
ec0eee9d3f implement scroll to currently playing feature (#247) 2025-11-09 13:49:29 -08:00
observer
140546ca4d log in catch statement in PlayerQueueFragment:onResume 2025-11-09 21:27:17 +00:00
eddyizm
fd075a02c5 chore: updating change log and pending changes 2025-11-09 09:33:23 -08:00
observer
4d1d953a3a implement scroll to currently playing feature 2025-11-09 16:03:02 +00:00
eddyizm
9dd509be22 feat: Make artist and album clickable (#243) 2025-11-09 07:06:41 -08:00
observer
efaae35976 uncap mediamanager queue previously at 25 at 500 2025-11-09 15:05:49 +00:00
observer
35784216bc use getRandomSampleWithGenre() rather than getSongsByGenre() 2025-11-09 15:05:05 +00:00
observer
8ce0a82506 getRandomSongs2(): use new subsonic getRandomSongs param 'genre' instead of getSongsByGenre 2025-11-09 15:02:54 +00:00
skajmer
51883cd82b Merge branch 'eddyizm:development' into development 2025-11-09 13:06:27 +01:00
eddyizm
829c5d85f6 fix: Images not filling holder (#244) 2025-11-08 19:31:36 -08:00
eddyizm
748a19ef44 fix: discovery image fills holder at start 2025-11-08 09:34:25 -08:00
eddyizm
e987226954 feat: makes discovery media item clickable on home page #53 2025-11-08 09:09:09 -08:00
Max
59e2f4a7fa Update strings.xml
Adding new translation strings
2025-11-07 15:04:56 +03:00
eddyizm
817b53efaa fix: Equalizer fix in main build variant (#239) 2025-11-06 17:22:56 -08:00
Jaime García
12c7ec86a9 fix: Add listener to enable equalizer when audioSessionId changes (main build variant) 2025-11-07 02:06:08 +01:00
eddyizm
611b5001be chore: bumped version for new tag 2025-11-06 16:08:55 -08:00
eddyizm
923cfd5bc9 fix: equalizer missing referenced value 2025-11-06 16:08:07 -08:00
eddyizm
36d2320e70 chore: bump version and changelog for release 2025-11-06 15:44:46 -08:00
eddyizm
17713ee400 fix: Add listener to enable equalizer when audioSessionId changes (#235) 2025-11-06 15:40:39 -08:00
eddyizm
ee3465868e Album track list bug (#237) 2025-11-06 15:40:14 -08:00
eddyizm
9e8141a8d9 chore: name version 2025-11-06 14:49:19 -08:00
eddyizm
043e1b39b0 Revert "fix: do not override itemType and itemId"
This reverts commit d35146dba3.
2025-11-06 14:35:29 -08:00
Jaime García
938c1de906 chore: Remove comment 2025-11-06 04:48:24 +01:00
Jaime García
a0dfe63660 fix: Add listener to enable equalizer when audioSessionId changes 2025-11-06 03:18:39 +01:00
skajmer
42b7441467 Merge branch 'eddyizm:development' into development 2025-11-05 22:20:18 +01:00
eddyizm
28c2f87b26 chore: updated change log 2025-11-05 09:15:11 -08:00
eddyizm
9ab22bfede chore: bump version 2025-11-05 07:51:55 -08:00
eddyizm
20900fb557 chore: pre release prep 2025-11-04 22:11:02 -08:00
eddyizm
7457c5b6e3 fix: skip mapping downloaded item (#228) 2025-11-04 15:43:04 -08:00
pca006132
e5a928ec0f fix: skip mapping downloaded item 2025-11-05 00:35:27 +08:00
eddyizm
147c8360a6 Revert "improve battery consumption" (#227) 2025-11-04 07:02:50 -08:00
eddyizm
d5d504fc64 Revert "improve battery consumption" 2025-11-04 07:02:19 -08:00
eddyizm
24eead2d0a improve battery consumption (#223) 2025-11-03 21:20:06 -08:00
pca006132
2644fa52b6 enable offload 2025-11-03 22:33:42 +08:00
pca006132
38c144c073 avoid rebuffering after track change 2025-11-03 22:33:21 +08:00
pca006132
1dca1ef68d avoid updating player bottom sheet when not visible 2025-11-03 16:16:06 +08:00
pca006132
ba94d7e5cc fix null 2025-11-03 16:01:15 +08:00
pca006132
0028872e3f update one media item only 2025-11-03 15:07:29 +08:00
pca006132
be9eec625a avoid full updates 2025-11-03 14:50:06 +08:00
pca006132
b335ddec01 cache artwork bitmap 2025-11-03 13:33:22 +08:00
skajmer
8ad35ce83a Merge branch 'eddyizm:development' into development 2025-11-02 18:15:01 +01:00
eddyizm
eb5c4721d1 fix: update MediaItems after network change (#222) 2025-11-02 08:01:43 -08:00
eddyizm
b0e8fa75ca chore: update media3 dependencies (#217) 2025-11-02 08:00:55 -08:00
eddyizm
27d7288ee9 fix: do not override getItemViewType and getItemId (#221) 2025-11-02 08:00:37 -08:00
eddyizm
287921de09 fix: playlist page should not snap (#218) 2025-11-02 07:59:51 -08:00
eddyizm
e5cb8793b0 fix: remove NestedScrollViews for fragment_album_page (#216) 2025-11-02 07:59:34 -08:00
eddyizm
f25e7f250a Fix downloaded tab performance (#210) 2025-11-02 07:59:10 -08:00
eddyizm
911acc3c2d feat: sort artists by album count (#206) 2025-11-02 07:58:46 -08:00
pca006132
4b7f60bb8c fix: update MediaItems after network change 2025-11-02 16:44:15 +08:00
pca006132
d35146dba3 fix: do not override itemType and itemId 2025-11-02 14:54:32 +08:00
eddyizm
6c3897a400 Update USAGE.md with instant mix details (#220) 2025-11-01 16:50:47 -07:00
Thomas Anderson
1002499d92 Update USAGE.md with instant mix details
Follow up of https://github.com/eddyizm/tempus/issues/184#issuecomment-3475333928
2025-11-01 22:43:09 +03:00
skajmer
4464b5b34d Merge branch 'eddyizm:development' into development 2025-11-01 09:52:55 +01:00
pca006132
77c0b86dac fix: playlist page should not snap 2025-11-01 13:34:47 +08:00
pca006132
0abdfc6b19 fix: remove NestedScrollViews for fragment_album_page 2025-11-01 13:31:05 +08:00
pca006132
52b2ca8fa7 chore: update media3 dependencies 2025-11-01 11:44:40 +08:00
pca006132
7f66124614 fix: download tab performance 2025-10-31 20:16:01 +08:00
eddyizm
9930537486 shuffle for artists without using getTopSongs (#207) 2025-10-30 18:54:19 -07:00
pca006132
5d51132921 feat: add preference to sort artists by album count 2025-10-30 20:01:47 +08:00
pca006132
4b2e963a81 fix: shuffle for artists without using getTopSongs 2025-10-30 20:01:28 +08:00
pca006132
4c865e199d feat: sort artists by album count 2025-10-30 18:01:07 +08:00
eddyizm
fc58869354 chore(i18n): Update Spanish (es-ES) translation (#205) 2025-10-29 17:47:03 -07:00
Jaime García
5e1a2b41e9 chore(i18n): Update Spanish (es-ES) translation 2025-10-29 22:06:13 +01:00
eddyizm
77bdd71d79 chore: updated bug issue 2025-10-29 09:29:36 -07:00
skajmer
7267c13ee0 Add #199 2025-10-29 12:38:15 +01:00
eddyizm
4ab1f034d8 chore: bumped version for release, added changelogs 2025-10-28 18:53:45 -07:00
eddyizm
4bd8bbfa4c chore: removed build badge since its not live yet 2025-10-28 18:39:41 -07:00
eddyizm
3fc03114e2 chore: updated github url 2025-10-28 18:37:45 -07:00
eddyizm
576c93e6cb Crash on share no expiration date or field returned from api (#199) 2025-10-27 20:57:36 -07:00
eddyizm
ac674d937a fix: handle null or no expiry field being sent back from server 2025-10-27 20:47:46 -07:00
eddyizm
0ed329022e fix: gradle updated to exclude offending blobs, change log link fixed, removed old screenshots. 2025-10-27 19:55:29 -07:00
eddyizm
b8b4a77349 chore: updated tempo references to tempus including github check (#197) 2025-10-27 09:08:13 -07:00
eddyizm
de14663b25 chore: updated tempo references to tempus including github check 2025-10-27 09:06:57 -07:00
eddyizm
747af0d81c fix: updated crash report and degoogled icon for izzydroid #195 2025-10-27 07:39:07 -07:00
eddyizm
c95e7cc5e0 fix: disabled workflow, manual build 2025-10-26 12:01:51 -07:00
eddyizm
0c3b43c5dc fix: build paths 2025-10-26 11:49:08 -07:00
eddyizm
830e9076f1 never surrender 2025-10-26 11:32:23 -07:00
eddyizm
cd9ae97bc7 fix: bumped version, update path from build error logs 2025-10-26 11:17:59 -07:00
eddyizm
1b59a8e8ef chore: bumped build version 2025-10-26 10:59:22 -07:00
eddyizm
391405fc76 fix: workflow broken, taking another approach 2025-10-26 10:58:18 -07:00
eddyizm
4bdcbacf62 chore: bumped version for new workflow build 2025-10-26 10:15:22 -07:00
eddyizm
6cbae700bf fix: type in the workflow yaml 2025-10-26 10:02:59 -07:00
eddyizm
e7555119f0 chore: bumped version, updated ignore file 2025-10-26 09:35:12 -07:00
eddyizm
e228e74e6a Update Polish translation (#188) 2025-10-26 07:27:11 -07:00
skajmer
062f4db2cf Merge branch 'eddyizm:development' into development 2025-10-26 09:53:04 +01:00
eddyizm
cb75e34b92 fix: updated work flow for new tempus release 2025-10-25 19:51:12 -07:00
eddyizm
36005c5f51 Tempus rebrand (#183) 2025-10-25 13:12:49 -07:00
eddyizm
8ae0900269 Merge branch 'development' into tempus-rebrand 2025-10-25 13:12:32 -07:00
eddyizm
f286c7b1b9 fix: readme update for new screenshot size 2025-10-25 13:10:58 -07:00
eddyizm
c5d0af67a7 chore: added a few more screenshots 2025-10-25 13:06:24 -07:00
skajmer
3fb4ccd791 Update strings.xml
#158, #161
2025-10-24 23:10:57 +02:00
eddyizm
577b50a85b chore: bumped to alpha to release debug apks 2025-10-22 21:45:03 -07:00
eddyizm
e6a56ba1d2 chore: update readme and usage references to tempus. added new banner… (#182) 2025-10-22 08:27:57 -07:00
eddyizm
2740b6da29 feat: added green launcher to degoogled variant 2025-10-22 07:55:13 -07:00
eddyizm
21c4ae77ba fix: splash logo sized to not crop anymore 2025-10-21 11:25:01 -07:00
eddyizm
7a83a03a90 feat: first pass swapping new icon, build config and rename folder structure 2025-10-21 09:23:25 -07:00
eddyizm
be0480538e Merge branch 'development' into tempus-readme-update 2025-10-20 12:22:25 -07:00
eddyizm
b48057b4a2 fix: persist album sorting on resume (#181) 2025-10-20 12:18:41 -07:00
eddyizm
fa430eaac4 Unhide genre from album details view (#161) 2025-10-20 12:18:11 -07:00
eddyizm
82ee9b4639 chore: reset changelog to 1 since its a new app 2025-10-20 09:27:31 -07:00
eddyizm
9b807fde31 fix: typo in spanish/port language, fixing capitalizion in git url 2025-10-19 18:25:34 -07:00
eddyizm
c7ba4235b3 chore: updated strings with tempus and updated screenshots. 2025-10-19 17:37:55 -07:00
eddyizm
d27e431f73 fix: typo in readme link 2025-10-19 11:18:19 -07:00
eddyizm
d23eea4f27 chore: updated fastlane docs, icon, and reset changelogs to start initial release 2025-10-19 10:08:23 -07:00
eddyizm
430e7105eb fix: updated html for banner 2025-10-19 08:19:48 -07:00
eddyizm
024c4e6118 chore: update readme and usage references to tempus. added new banner logo 2025-10-18 22:19:03 -07:00
eddyizm
7b2ee9da3a fix: updated workflow for 32/64 bit apks (#176) 2025-10-18 17:27:00 -07:00
eddyizm
c3cce18600 fix: persist album sorting on resume 2025-10-18 17:16:11 -07:00
sebaFlame
442fe1ea01 Merge branch 'development' into main 2025-10-18 01:48:47 +02:00
cba
cb0874dca4 Added setting to make album detail visible 2025-10-18 01:00:00 +02:00
cba
079149c1d5 Revert "removed dropdown for album info"
This reverts commit ceaffa254b.
2025-10-18 00:37:06 +02:00
eddyizm
118f742cb6 Check also underlying transport (#90) 2025-10-17 06:46:56 -07:00
Thomas Anderson
c028c52576 Merge branch 'main' into patch-1 2025-10-17 12:26:42 +03:00
eddyizm
96c5d0fca8 fix: updated workflow for 32/64 bit apks 2025-10-16 21:54:53 -07:00
eddyizm
e39a5e2d5c chore: updated changelog 2025-10-16 17:19:49 -07:00
eddyizm
a06ab77b42 chore: bumped version for release 2025-10-16 14:26:14 -07:00
eddyizm
04a6176bfd fix: limits image size to prevent widget crash #172 (#175) 2025-10-16 13:25:25 -07:00
eddyizm
1f4464e089 fix: Include shuffle/repeat controls in f-droid build's media notific… (#174) 2025-10-16 13:25:04 -07:00
eddyizm
9d01d2057a fix: limits image size to prevent widget crash #172 2025-10-15 21:57:24 -07:00
eddyizm
ad440c490a Fix album parse empty date field (#171) 2025-10-15 07:26:22 -07:00
le-firehawk
acdcfff9ac fix: Include shuffle/repeat controls in f-droid build's media notification window 2025-10-15 21:00:39 +10:30
eddyizm
8c7a25cbd0 fix: update to handle nulls in the sort function 2025-10-13 21:49:41 -07:00
eddyizm
bdca5e16ed Merge branch 'development' into fix-album-parse-empty-date-field 2025-10-13 21:10:01 -07:00
eddyizm
f091b3d248 fix: handle empty date fields from subsonic json 2025-10-13 21:09:27 -07:00
eddyizm
18cd84f820 fix: persist album sort preference (#168) 2025-10-13 16:53:06 -07:00
eddyizm
281ebf8263 fix: General build warning and playback issues (#167) 2025-10-13 06:59:33 -07:00
eddyizm
2854ac6354 fix: persist album sort preference. 2025-10-12 22:12:53 -07:00
eddyizm
16d25a1f1d Merge branch 'main' into development 2025-10-12 09:57:55 -07:00
eddyizm
5d3ca8acfa chore: added multi library documentation 2025-10-12 09:57:35 -07:00
eddyizm
0689272046 fix: workflow trigger updated for my tagging convention 2025-10-12 09:56:52 -07:00
le-firehawk
17372fc4d0 fix: Resolve parcel serialization build warnings 2025-10-12 23:20:47 +10:30
le-firehawk
44679855cd fix: Replace poor syntax that created warnings during build 2025-10-12 23:20:47 +10:30
le-firehawk
78e7032903 fix: When creating MediaService, restore player from previous queue 2025-10-12 23:20:47 +10:30
eddyizm
8d8087f2d6 chore: fix some grammar in readme 2025-10-11 09:07:38 -07:00
eddyizm
5b6a4fab62 chore: updated changelog 2025-10-10 22:30:55 -07:00
eddyizm
fdc41b299c chore: updated ignore file for release apk files 2025-10-10 22:29:41 -07:00
eddyizm
82c22ed247 chore: bumped version for release 2025-10-10 22:17:09 -07:00
eddyizm
48ce3a2a4f chore: updated release info 2025-10-10 22:02:01 -07:00
eddyizm
b93acc6563 fix: Glide module incorrectly encoding IPv6 addresses (#159) 2025-10-09 22:18:49 -07:00
eddyizm
9c088a7e88 feat: Make all objects in Tempo references for quick access (#158) 2025-10-09 22:18:11 -07:00
eddyizm
de2f1067a7 chore: Update Polish translation (#160) 2025-10-09 22:11:19 -07:00
eddyizm
1c21546461 chore: adding screenshot and docs for 4 icons/buttons in player control (#162) 2025-10-09 21:54:38 -07:00
eddyizm
a4121e8d49 chore: adding screenshot and docs for 4 icons/buttons in player control 2025-10-09 21:53:52 -07:00
cba
ceaffa254b removed dropdown for album info 2025-10-09 23:03:25 +02:00
skajmer
4cc4cc7363 Update Polish translation
Stuff from:
#140 
#135 
#98 
#152
2025-10-09 21:41:12 +02:00
le-firehawk
c5ef274916 fix: Glide module incorrectly encoding IPv6 addresses 2025-10-09 23:14:54 +10:30
le-firehawk
2c53f36a18 fix: Support content URIs for external downloader 2025-10-09 23:03:57 +10:30
le-firehawk
6c637dcbcb feat: Make all objects in Tempo references for quick access 2025-10-09 23:03:57 +10:30
eddyizm
89fa38f5a0 chore: updated change log 2025-10-08 21:55:00 -07:00
eddyizm
87f8bdc618 chore: bump version and remove play details from build 2025-10-08 21:50:10 -07:00
eddyizm
903fde4bdc fix: Replace hardcoded strings in SettingsFragment (#152) 2025-10-08 21:38:49 -07:00
eddyizm
3824dd882c Merge branch 'development' into fix-hardcoded-strings 2025-10-08 21:38:31 -07:00
eddyizm
602bab6414 feat: Show sampling rate and bit depth in downloads (#154) 2025-10-08 21:30:54 -07:00
eddyizm
d891e429b6 fix: updating release workflow to account for the 32/64 bit builds an… (#156) 2025-10-08 21:30:11 -07:00
eddyizm
ebefd77027 chore: removed play variant (#155) 2025-10-08 21:29:59 -07:00
eddyizm
57f34affd9 fix: Re-add new equalizer settings that got lost (#153) 2025-10-08 21:29:40 -07:00
eddyizm
50b5ab38bc chore(i18n): Update Spanish translation (#151) 2025-10-08 21:27:15 -07:00
eddyizm
9f61d70fca fix: updating release workflow to account for the 32/64 bit builds and degoogled variant 2025-10-08 21:26:06 -07:00
eddyizm
a97a2d5b50 chore: removed play variant 2025-10-08 21:10:45 -07:00
Jaime García
8d73a2cd36 fix: Re-add new equalizer settings that got lost 2025-10-09 04:52:40 +02:00
Jaime García
ca5a0698bb feat: Show sampling rate and bit depth in downloads 2025-10-09 04:32:22 +02:00
Jaime García
04f34e03d1 fix: Replace hardcoded strings in SettingsFragment 2025-10-08 22:17:30 +02:00
Jaime García
233bc9987e chore(i18n): Update Spanish translation 2025-10-08 21:05:55 +02:00
Jaime García
f0e418687e chore(i18n): Update Spanish translation 2025-10-08 20:41:47 +02:00
eddyizm
e87b658447 chore: random blank file i probably added 2025-10-07 22:20:55 -07:00
eddyizm
19c985c9e4 chore: updated changelog 2025-10-07 22:20:19 -07:00
eddyizm
4ab122a9d7 Merge branch 'development' 2025-10-07 21:52:10 -07:00
eddyizm
ff0c42d14c fix: added preference that got lost in merging, removed old media factory ref, bumped version for release 2025-10-07 21:51:02 -07:00
eddyizm
d1e247f9e2 Merge branch 'feature-widget-playing' into development 2025-10-07 21:31:44 -07:00
eddyizm
f1d19142fa feat: Add home screen music playback widget and some updates in Turkish localization (#98) 2025-10-07 21:28:10 -07:00
eddyizm
45793c343a chore: formatting 2025-10-07 21:26:36 -07:00
eddyizm
aa4249842d Sin tan1729 skip duplicates (#149) 2025-10-06 22:25:30 -07:00
eddyizm
126663f1e5 Merge branch 'development' into SinTan1729-skip-duplicates 2025-10-06 22:25:17 -07:00
eddyizm
ec19e8c401 feat: Added support for skipping duplicates (#135) 2025-10-06 22:23:06 -07:00
eddyizm
717f95a04a Merge branch 'development' into skip-duplicates 2025-10-06 22:22:52 -07:00
eddyizm
ccce01a61b Merge branch 'playlist-duplicates' into SinTan1729-skip-duplicates 2025-10-06 21:49:05 -07:00
eddyizm
a7682d7656 fix: updated settings stitles, merge conflict 2025-10-06 21:43:30 -07:00
eddyizm
84de93a4f1 Merge branch 'development' into playlist-duplicates 2025-10-06 21:27:29 -07:00
eddyizm
78c4c89eca feat: Support user-defined download directory for media (#21) 2025-10-06 21:16:17 -07:00
SinTan1729
328beaff90 fix: Use string for settings section name 2025-10-06 22:04:23 -05:00
SinTan1729
9d5d89d648 new: Separate toast for when all songs were skipped
Also, fixed grammatical issue in toast where they were all singular.
2025-10-06 21:58:41 -05:00
SinTan1729
cd8b06f544 fix: Reverted old toast while adding to playlist 2025-10-06 21:50:00 -05:00
SinTan1729
47a0def06c fix: Removed unnecessary code 2025-10-06 21:48:42 -05:00
SinTan1729
1c2f1aa061 chg: Move the playlist duplicates option to preferences
As per the recommendation of @eddyizm
2025-10-06 21:47:57 -05:00
eddyizm
30281e8f2d fix: had the heart icons inverted 2025-10-06 15:32:54 -07:00
eddyizm
3c58e6fbb2 chore: added sha256 signing key for verification (#147) 2025-10-05 13:33:06 -07:00
eddyizm
99a399b4d7 chore: added sha256 signing key for verification 2025-10-05 13:31:46 -07:00
eddyizm
1da0a0b810 Unify and update polish translation (#146) 2025-10-05 13:03:17 -07:00
eddyizm
539920965e Notification heart rating (#140) 2025-10-05 13:01:44 -07:00
eddyizm
9a64eeabe6 feat: added preference to disable heart and show shuffle instead 2025-10-05 12:59:24 -07:00
eddyizm
791190f681 Merge branch 'development' into notification-heart-rating 2025-10-05 08:52:17 -07:00
skajmer
c03fca8039 Unify and update translation
Unified so the strings are in the same places as they are in the english version which makes editing stuff easier. Updated existing strings, and added some that were missing.
2025-10-05 13:38:29 +02:00
le-firehawk
620fba0a14 fix: Prevent externalAudioReader from hogging the main thread 2025-10-04 23:33:48 +09:30
le-firehawk
1357c5c062 feat: Integrate external downloads into downloaded songs view 2025-10-04 23:33:48 +09:30
le-firehawk
682f63ef38 feat: Add metadata caching and proper integration for external media files 2025-10-04 23:33:48 +09:30
le-firehawk
24864637f9 feat: Hook external audio write into file cache from external audio reader, fix download notifications 2025-10-04 23:33:48 +09:30
le-firehawk
3ba19be4d9 feat: Load media downloaded as file for offline use 2025-10-04 23:33:48 +09:30
le-firehawk
cce6456951 feat: Support user-defined download directory for media 2025-10-04 23:33:48 +09:30
eddyizm
fda586c4d8 chore: add link to discussion page in settings (#143) 2025-10-02 21:33:25 -07:00
eddyizm
57be72d5d4 chore: add link to discussion page in settings 2025-10-02 21:32:34 -07:00
eddyizm
c2354d4d42 fix: Lag during startup when local url is not available (#110) 2025-10-02 07:10:32 -07:00
eddyizm
a940af934c feat: notification-heart-rating 2025-10-01 22:27:26 -07:00
eddyizm
5891ec800c chore: groundwork for heart rating 2025-09-30 15:41:58 -07:00
eddyizm
7259a82b67 feat: Enable downloading of song lyrics for offline viewing (#99) 2025-09-29 07:02:14 -07:00
le-firehawk
c2b6d7eed5 feat: Enable downloading of song lyrics for offline viewing 2025-09-29 22:04:38 +09:30
eddyizm
2335bf2095 fix: removed universal ref causing build issues 2025-09-28 18:59:21 -07:00
eddyizm
8bb6c02e46 feat: download starred artists. (#137) 2025-09-28 16:17:37 -07:00
eddyizm
47380a79a5 fix: added init on home tab and dialog, refactor and check for songs for albums/artists before displaying dialog 2025-09-28 16:14:42 -07:00
eddyizm
a187ba1e75 fix: moved api call back to artist repository after losing the thread. 2025-09-27 22:37:30 -07:00
eddyizm
3eb9b2fb5c chore: added dialog to starred artists sync 2025-09-27 21:52:04 -07:00
eddyizm
a547e19361 Merge branch 'development' into Sync-starred-artists-offline 2025-09-27 18:25:20 -07:00
eddyizm
f4722fa0a8 fix: Update search query validation to require at least 2 characters instead of 3 (#124) 2025-09-27 18:17:23 -07:00
eddyizm
ee738bc4c7 feat: download starred artists. 2025-09-27 15:37:59 -07:00
SinTan1729
a22883fdde fix: The layout should be more in line with the playlist entries 2025-09-26 22:51:52 -05:00
SinTan1729
2acf11023a fix: Crash when trying to add to an empty playlist 2025-09-26 19:19:23 -05:00
SinTan1729
9736890e3c fix: Show proper number in add to playlist dialog toast 2025-09-26 16:48:56 -05:00
SinTan1729
e790bf3eb6 chg: Comment out unused code 2025-09-26 16:39:46 -05:00
SinTan1729
e1d63a9eef feat: Support skipping duplicates 2025-09-26 16:24:21 -05:00
SinTan1729
134a1605ad fix: Get rid of the try-catch since it's considered bad practice in Java
This matches the treatment done at other places in the code, so it
should be fine.
2025-09-26 05:56:48 -05:00
eddyizm
1b45036963 fix: removed universalApk ref from build 2025-09-24 22:05:34 -07:00
eddyizm
0ba12c3d84 Update French localization (#125) 2025-09-24 09:25:40 -07:00
Benoît Smith
8cc3356b14 Update strings.xml 2025-09-24 11:38:40 +02:00
Jaime García
9d439b726b fix: Update search query validation to require at least 2 characters instead of 3 2025-09-24 04:03:51 +02:00
eddyizm
d4c0e30fd1 fix: Prevent crash when getting artist radio and song list is null (#117) 2025-09-23 17:34:24 -07:00
eddyizm
bb23d7e866 chore: updated changelog with latest updates 2025-09-23 15:34:07 -07:00
eddyizm
7321ef46f2 v3.15.0 (#118) 2025-09-23 15:23:41 -07:00
eddyizm
2fe2c2b28b chore: version bump 2025-09-23 15:21:43 -07:00
Jaime García
a9318ec5d0 fix: Prevent crash when getting artist radio and song list is null 2025-09-23 23:45:00 +02:00
eddyizm
eb29dc2fb2 feat: Tap anywhere on the song item to toggle playback (#112) 2025-09-23 12:17:56 -07:00
eddyizm
287e4a2b10 fix: add listener to track playlist click/change (#113) 2025-09-23 12:16:22 -07:00
eddyizm
b7d56c2d70 fix: null check for scrobble when disconnecting from chromecast, which was crashing app 2025-09-23 12:03:04 -07:00
eddyizm
5a6d101bdf fix: playlist selection working now 2025-09-23 09:34:59 -07:00
Jaime García
969f0b5b21 feat: Replace play/pause button with an icon, allow tapping on full item to play/pause song in song lists 2025-09-23 17:55:18 +02:00
Jaime García
14939d20fd feat: Replace play/pause button with an icon, allow tapping on full item to play/pause song in Queue 2025-09-23 17:55:17 +02:00
mucahit-kaya
35af1f9038 fix(widget): refine layouts and progress UX across sizes
Compact (4×1)
- Reduce root vertical padding so the 4×1 cell yields ~56dp of content height.
- Make album art a true square (50×50dp) and center vertically; keeps edges
  clear of rounded corners.
- Tighten timing block: 2dp progress bar; 10sp labels with no extra font
  padding; prevents elapsed/total text from slipping below the background.
- Wrap album art in a 50×50dp FrameLayout with a new 6dp-radius background
  drawable; soft corners while remaining visually smaller than the widget body.
- Mirror the same structure in the preview layout so Studio preview matches
  on-device rendering.
  (app/src/main/res/layout/widget_layout_compact.xml,
   app/src/main/res/drawable/widget_album_art_bg.xml)

Large Short (4×2)
- Wrap album art in a fixed 90dp square container and enforce a true square
  crop via centerCrop.
- Tighten vertical spacing: thinner progress bar, closer timing row, controls
  shifted down for better balance.
- Keep album/timing text to the left of the controls but retune spacing so the
  stack stays fully inside the widget bounds.

Large (4×3 and up)
- Restructure to a vertical stack: header row (album art + text), full-width
  progress bar, timing row, primary controls, then secondary controls.
- Lock album art to a 150dp square; progress bar spans the widget beneath the
  header to match the new visual hierarchy.

Based-on: cd28ee0764
Co-authored-by: The Firehawk <firehawk@opayq.net>
Co-Authored-By: Mücahit Kaya <kaya-mucahit@outlook.com>
Co-Authored-By: Firehawk <firehawk@opayq.net>
2025-09-23 14:32:01 +02:00
mucahit-kaya
b79cfa4af0 fix(widget): resume progress updates during playback
The widget was only updating on play/pause state changes, so the timer
did not advance while playback continued. Added a Handler loop in
MediaService that updates the widget every second while playback is
running and clears it when playback stops, ensuring the progress bar
refreshes regularly.

Co-Authored-By: Firehawk <firehawk@opayq.net>
2025-09-23 14:32:00 +02:00
le-firehawk
e81e1a5356 fix: Include song position and duration in widget
Co-authored-by: Mücahit Kaya <kaya-mucahit@outlook.com>
Co-authored-by: The Firehawk <firehawk@opayq.net>
2025-09-23 14:29:21 +02:00
mucahit-kaya
cc0e264a17 feat: Add home screen music playback widget
Introduces a new app widget for music playback control and display. Adds widget provider classes, update manager, view factory, and related resources (layouts, colors, strings, XML). Integrates widget updates with MediaService to reflect current playback state. Updates AndroidManifest to register the widget.
2025-09-23 14:29:21 +02:00
SinTan1729
a83495f353 fix: Removed unnecessary imports 2025-09-23 02:30:22 -05:00
SinTan1729
be4346b3d1 fix: Lag during startup when local url is not available 2025-09-23 02:26:26 -05:00
eddyizm
2e29e9537a feat: Mark currently playing song with play/pause button (#107) 2025-09-22 12:40:33 -07:00
eddyizm
bc0adfe8e0 feat: added 32bit build and debug build for testing. Removed unused f… (#108) 2025-09-22 12:38:32 -07:00
eddyizm
5261ca317b feat: added 32bit build and debug build for testing. Removed unused function after merging changes for media service 2025-09-22 12:32:33 -07:00
eddyizm
bf4ff3f1f9 Updates to polish translation (#105) 2025-09-22 12:15:55 -07:00
Jaime García
cd195dbba0 refactor: Remove unused import 2025-09-22 20:21:27 +02:00
Jaime García
7ec78991a5 refactor: Rename methods and variables 2025-09-22 20:10:57 +02:00
Jaime García
e1c5a60805 refactor: Rename methods and variables 2025-09-22 20:03:02 +02:00
Jaime García
f74813ef69 refactor: Remove commented code 2025-09-22 19:47:07 +02:00
Jaime García
040558198e refactor: Add some code mistakenly removed, remove some comments, remove unused parameter 2025-09-22 19:39:53 +02:00
Jaime García
5ab68e4a98 feat: Add play/pause button in song lists 2025-09-22 19:28:01 +02:00
skajmer
aa8fac43a6 Update strings.xml
New strings (mainly the EQ ones)
2025-09-22 17:37:37 +02:00
Jaime García
905bb3e3c5 fix: Use proper play icon 2025-09-22 01:31:34 +02:00
Jaime García
d810010090 feat: Mark currently playing song in PlayerSongQueueAdapter 2025-09-22 00:35:23 +02:00
Jaime García
52ba783a90 feat: Mark currently playing song in SongHorizontalAdapter 2025-09-22 00:15:52 +02:00
eddyizm
d72855e160 fix: Resolve playback issues with live radio MPEG & HLS streams (#89) 2025-09-21 11:59:28 -07:00
Jaime García
82a9f00173 Merge branch 'development' into fix-live-radio-streams 2025-09-21 19:27:21 +02:00
eddyizm
3f5749f7e1 feat: Built-in audio equalizer (#94) 2025-09-21 10:20:49 -07:00
eddyizm
cd2ab36351 16 bug it only plays the first song on an album (#81) 2025-09-21 09:56:26 -07:00
eddyizm
a6688f897a chore: removed comments 2025-09-21 09:30:41 -07:00
eddyizm
64658dda1f Update Korean translations (#97) 2025-09-21 06:13:24 -07:00
WooJin Kong
d9f701d9d3 feat: Update korean translations 2025-09-14 01:55:24 +09:00
Jaime García
60fee3c77c fix: Allow only integer values in equalizer seek bars, show positive dB values with plus symbol 2025-09-09 02:42:52 +02:00
Jaime García
b89086c5be fix: Prevent switch enable animation when opening equalizer fragment 2025-09-09 02:11:36 +02:00
Jaime García
d3dd236054 feat: Hide Equalizer button in player when it is not available 2025-09-09 01:46:06 +02:00
Jaime García
2e3330b63f feat: Hide Equalizer option when it is not available 2025-09-09 01:29:47 +02:00
Jaime García
e604c9ba86 chore(i18n): Update equalizer option summary string in Spanish 2025-09-08 22:47:29 +02:00
Jaime García
2bf39d846e style: Convert margin values to density-independent pixels in EqualizerFragment 2025-09-08 22:06:26 +02:00
Jaime García
06066f1f66 refactor: Remove redundant null checks after loading equalizer band levels 2025-09-08 20:08:05 +02:00
Jaime García
7c0d44680f feat: Add audio equalizer with UI 2025-09-08 19:28:34 +02:00
Jaime García
d4cb6c5c9a fix: Update MediaService in all build variants 2025-09-06 00:20:35 +02:00
Jaime García
fab18c130e fix: Support HLS (.m3u8) streams with parameters in URL 2025-09-05 21:29:11 +02:00
Thomas Anderson
ffcfd81c28 Check also underlaying transport 2025-09-05 15:45:00 +03:00
Jaime García
bd753f4489 fix: Use defined media type for live radio detection, relocate DynamicMediaSourceFactory 2025-09-05 11:19:47 +02:00
Jaime García
e43a2b6fe5 fix: Resolve playback issues with live radio MPEG & HLS streams 2025-09-05 04:46:01 +02:00
eddyizm
c62d2ace4d Update RU locale (#87) 2025-09-03 20:00:57 -07:00
Denis Bezykornov
6d403f808c Update RU locale 2025-09-03 19:28:28 +03:00
eddyizm
1223062388 Create FUNDING.yml 2025-09-02 07:57:51 -07:00
eddyizm
b0ddd5388b Update French localization (#84) 2025-09-02 07:11:05 -07:00
Benoît Smith
92f79a8e3d Update strings.xml (FR) [2] 2025-09-02 10:49:16 +02:00
Benoît Smith
473d7e4e9c Update strings.xml (FR) 2025-09-02 10:48:04 +02:00
eddyizm
7ca0415274 chore: bumped version 2025-08-31 20:25:32 -07:00
eddyizm
cf7feacdc0 fix: casting full album/playlist working as intended, passing metadata for album artwork in small square #16 2025-08-31 20:24:48 -07:00
eddyizm
4740028a44 chore: updated version for release 2025-08-31 13:23:33 -07:00
eddyizm
fe2c163aaa bug fixes, chores, docs v3.14.8 (#80) 2025-08-31 12:43:06 -07:00
eddyizm
c6f08d9cec chore: started usage docs, added a couple of discussion items, created layout 2025-08-31 12:38:13 -07:00
eddyizm
59b40df9ef fix: Disable "sync starred tracks/albums" switches when Cancel is clicked in warning dialog, use proper view for "Sync starred albums" dialog (#79) 2025-08-31 12:36:35 -07:00
eddyizm
81726baa08 style: Center subtitle text in empty_download_layout in fragment_download.xml when there is more than one line (#78) 2025-08-31 12:36:15 -07:00
eddyizm
af92a7b11a chore(i18n): Update Spanish (es-ES) and English translations (#77) 2025-08-31 12:35:40 -07:00
Jaime García
0578745bee fix: use "DialogStarredAlbumSync" view binding instead of "DialogStarredSync" 2025-08-31 17:48:56 +02:00
Jaime García
e24063e460 fix: disable sync starred tracks/albums switches in when Cancel is clicked in warning dialog 2025-08-31 17:39:35 +02:00
Jaime García
4f1b1b603e style: center subtitle text in empty_download_layout in fragment_download.xml when there is more than one line 2025-08-31 17:05:27 +02:00
Jaime García
7279c62944 chore(i18n): update Spanish translation 2025-08-31 16:44:00 +02:00
eddyizm
a59d46f884 Merge branch 'development' of github.com:eddyizm/tempo into development 2025-08-31 07:39:22 -07:00
eddyizm
10285b308d fix: Use correct SearchView widget to avoid crash in AlbumListPageFragment (#76) 2025-08-31 07:38:04 -07:00
Jaime García
8e2c5d1fee chore(i18n): update English translation 2025-08-31 16:32:01 +02:00
Jaime García
f59a360eb7 fix: Use correct SearchView widget to avoid crash in AlbumListPageFragment 2025-08-31 15:32:56 +02:00
eddyizm
accf5fddc2 chore: changelog update 2025-08-30 11:36:42 -07:00
eddyizm
cc61d1cd48 v3.14.1 release (#74) 2025-08-30 11:00:57 -07:00
eddyizm
6a16159cf0 fix: forgot sync album dialog, bump version for release 2025-08-30 10:58:38 -07:00
eddyizm
eaf2710054 fix: minor refactor of sync album observer 2025-08-30 09:23:44 -07:00
eddyizm
31d91f7215 feat: adds sync starred albums functionality #66 (#73) 2025-08-30 09:06:47 -07:00
eddyizm
f854f49686 feat: adds sync starred albums functionality #66 2025-08-30 09:04:25 -07:00
eddyizm
9e8870a86a Update French localization (#70) 2025-08-29 14:11:23 -07:00
Benoît Smith
a0040c52a0 More updates for strings.xml (FR) 2025-08-28 11:59:57 +02:00
Benoît Smith
65f6347faf Merge branch 'eddyizm:development' into development 2025-08-28 11:18:12 +02:00
Benoît Smith
85fa2f768e Update strings.xml (FR) 2025-08-28 11:17:32 +02:00
eddyizm
cc5abd150a fix: artist filtering in library view browse artist resolves #45 (#69) 2025-08-27 18:32:46 -07:00
eddyizm
1ed6ac6cff fix: artist filtering in library view browse artist resolves #45 2025-08-27 18:29:14 -07:00
eddyizm
cc6cb077b4 fix: catches null value and prepares bundle appropriately adding sing… (#64) 2025-08-26 21:59:33 -07:00
eddyizm
4be0acf76c fix: catches null value and prepares bundle appropriately adding single track to expected array list. closes #58 2025-08-26 21:57:20 -07:00
eddyizm
7d843390db setting-to-hide-song-rating (#60) 2025-08-25 20:49:30 -07:00
eddyizm
5c5316055c feat: setting to show/hide 5 star rating on playerview (#59)
First pass, I was not able to get the setting to update without having to restart the app. My attempt at using live data was missing something so I will have to revisit this when I get a better hang of it.
2025-08-25 20:42:09 -07:00
eddyizm
f1a179e7f8 Merge branch 'development' into setting-to-hide-song-rating 2025-08-24 19:14:36 -07:00
eddyizm
0377c5e939 feat: setting to show/hide 5 star rating on playerview 2025-08-24 18:55:36 -07:00
eddyizm
614ce8b466 style: Add song rating bar in landscape player controller layout (#57) 2025-08-24 16:33:50 -07:00
eddyizm
9e87b53bc9 feat: rating dialog added to album page (#52) 2025-08-24 16:32:50 -07:00
Jaime García
08023026b4 style: Add song rating bar in landscape player controller layout 2025-08-24 19:24:05 +02:00
eddyizm
1bbcf6c790 feat: rating dialog added to album page 2025-08-23 17:57:45 -07:00
eddyizm
698ca3b22b chore: changelog update 2025-08-23 14:05:01 -07:00
eddyizm
b2d875ac98 Merge pull request #51 from eddyizm/development
Development
2025-08-23 13:52:44 -07:00
eddyizm
02eef97171 chore: bumping version for release 2025-08-23 13:48:00 -07:00
eddyizm
26a5fb029a fix: moved hardcoded italian save text to string template, updated with english and italian language xmls 2025-08-23 13:04:17 -07:00
eddyizm
c38c7c3deb Merge pull request #50 from mucahit-kaya/development
feat: Add Turkish localization (values-tr)
2025-08-22 22:16:06 -07:00
eddyizm
8ed0a4642b chore: adding a note/not fully baked label to the sync user play queue setting #47 2025-08-22 22:12:22 -07:00
mucahit-kaya
6cfa04d368 Add Turkish language 2025-08-22 17:08:08 +02:00
eddyizm
af98f8d1a9 Merge pull request #44 from jaime-grj/rating-position
style: Change position and size of rating container
2025-08-18 15:17:53 -07:00
Jaime García
469204daac style: Change position and size of rating container 2025-08-16 02:43:02 +02:00
eddyizm
8943faf44c chore: updated change log 2025-08-15 10:42:03 -07:00
eddyizm
fea6366d84 Merge pull request #42 from eddyizm/development
Development - v3.12.0 release set up
2025-08-15 10:08:31 -07:00
eddyizm
92ac2e5684 chore: updated readme and bumped version for release. 2025-08-15 09:29:45 -07:00
eddyizm
06a52afa18 Merge pull request #40 from eddyizm/18-show-rating-on-song-view
feat: show rating on song view
2025-08-14 22:08:35 -07:00
eddyizm
87f6db9e79 chore: cleaned up dev log imports, commented code 2025-08-14 21:54:25 -07:00
eddyizm
5fa46cc49b feat:show rating on song view and allow setting/updating. #17 fixed a fr string error 2025-08-14 21:46:33 -07:00
eddyizm
4da967910a Merge pull request #39 from benoit-smith/development
Update French localization
2025-08-14 12:46:11 -07:00
Benoît Smith
3b18f39948 Update French localization 2025-08-14 10:56:28 +02:00
eddyizm
c9e0581815 Merge branch 'development' into 18-show-rating-on-song-view 2025-08-12 21:25:51 -07:00
eddyizm
b0fcc31f7b Merge pull request #38 from jaime-grj/fix-playercodecbitrateinfo
feat: added transcoding codec and bitrate info to PlayerControllerFragment, replace hardcoded strings
2025-08-12 21:24:26 -07:00
eddyizm
e98c9483c8 Merge pull request #37 from jaime-grj/fix-trackinfodialog
fix: Show placeholder string in TrackInfoDialog fields when there is no data
2025-08-12 21:23:45 -07:00
Jaime García
98a45b6059 fix: added transcoding codec and bitrate info to PlayerControllerFragment, replace hardcoded strings with dynamic values 2025-08-11 22:26:30 +02:00
Jaime García
6e070dfef0 fix: Avoid showing radio stream URL in Artist field of TrackInfoDialog 2025-08-11 20:58:06 +02:00
eddyizm
910cce90f5 Merge pull request #36 from benoit-smith/development
Update French localization
2025-08-11 07:11:15 -07:00
Jaime García
1a70ccd8f4 fix: Show placeholder string in TrackInfoDialog fields when there is no size and duration 2025-08-11 14:29:19 +02:00
Benoît Smith
7830657fe1 Update strings.xml French localization (more) 2025-08-11 10:56:06 +02:00
Benoît Smith
0351ccfc95 Update strings.xml French localization 2025-08-11 10:46:19 +02:00
Benoît Smith
10af6fb4ce Update arrays.xml French localization 2025-08-11 09:41:50 +02:00
Jaime García
61ec15e696 fix: Show placeholder string in TrackInfoDialog fields when there is no year, track number, bitrate and/or disc number 2025-08-11 03:02:01 +02:00
eddyizm
d21bd475a1 wip: initial new rating on song layout 2025-08-10 12:54:05 -07:00
eddyizm
7e34f6ee64 Merge branch 'development' of github.com:eddyizm/tempo into development 2025-08-10 07:50:05 -07:00
eddyizm
7cfefe76cc fix: removed duplicated screenshots from readme. 2025-08-10 07:49:28 -07:00
eddyizm
4585533740 Merge pull request #30 from skajmer/tempofork
Translations for sections
2025-08-10 07:40:43 -07:00
skajmer
07c1760c39 Additional strings
from: eddyizm/tempo/issues/29
2025-08-10 10:26:22 +02:00
eddyizm
24d4e67872 Merge pull request #33 from jaime-grj/fix-offlinemodetext
style: increased "Offline mode" text size, changed its color in dark theme
2025-08-09 22:48:21 -07:00
eddyizm
bec840620c Merge pull request #31 from BreadWare92/feat/i18n-german-track-info-home-section
feat(i18n): add German translations for track info and home section strings (#29)
2025-08-09 22:44:08 -07:00
Jaime García
2fa4ddf874 style: increased "Offline mode" text size, changed its color in dark theme 2025-08-10 02:10:20 +02:00
Matthias Reihs
e16f88cb73 feat(i18n): add German translations for track info and home section strings (#29) 2025-08-09 23:43:11 +02:00
skajmer
f79b05cb67 Translations for sections 2025-08-09 23:13:23 +02:00
eddyizm
ed7c572578 chore: updated ignore for release files 2025-08-09 11:14:56 -07:00
eddyizm
e891214831 Merge pull request #28 from eddyizm/development
v3.11.2 - merging to main
2025-08-09 11:03:15 -07:00
eddyizm
3d5ef1d900 chore: more version syncing 2025-08-09 10:56:18 -07:00
eddyizm
ba09f1f9ac chore: updated readme and change log 2025-08-09 10:51:09 -07:00
eddyizm
7ccc0a0cf8 chore: adding the new json schme and bumping the versions for release 2025-08-09 10:39:46 -07:00
eddyizm
4dfd346da5 chore: added changelog to track updates since fork 2025-08-09 10:27:11 -07:00
eddyizm
f9cc5b39cd Merge pull request #27 from jaime-grj/fix-strings-home
fix: make hardcoded strings in home fragment dynamic
2025-08-09 07:18:34 -07:00
eddyizm
04580e380b Merge pull request #26 from jaime-grj/fix-languageselection
fix: show "System default" language option, sort languages alphabetically, include country when showing language in settings
2025-08-09 07:12:28 -07:00
Jaime García
77ac195f32 fix: make hardcoded strings in home fragment dynamic 2025-08-09 13:22:25 +02:00
Jaime García
f53461382c fix: include country when showing language in settings 2025-08-09 02:53:20 +02:00
Jaime García
8a57f8f389 fix: show "System default" language option, sort languages alphabetically 2025-08-09 02:52:07 +02:00
eddyizm
0e76504cda Merge pull request #25 from jaime-grj/fix-localnetwork
fix: check for IP connectivity instead of Internet access
2025-08-08 14:09:04 -07:00
Jaime García
7fdffa7f6f fix: check for IP connectivity instead of Internet access 2025-08-08 22:15:52 +02:00
eddyizm
76293af038 Merge pull request #24 from jaime-grj/fix-albumpage
fix: hide unnecessary TextViews in AlbumPageFragment when there is no data, fixed incorrect album release date
2025-08-06 19:53:18 -07:00
Jaime García
e1aeccdd85 fix: hide genre in AlbumPageFragment properly 2025-08-07 04:18:25 +02:00
eddyizm
ab0e58b506 Merge pull request #22 from jaime-grj/samplerate-bitdepth
feat: show sampling rate and bit depth if available
2025-08-06 18:50:33 -07:00
Jaime García
98db83f11a fix: set album year in AlbumPageFragment 2025-08-07 03:26:04 +02:00
Jaime García
d49d37d1fd chore: remove comment 2025-08-07 03:06:01 +02:00
Jaime García
cc4d4701e1 fix: don't show empty year TextView if album has no year 2025-08-07 03:00:20 +02:00
Jaime García
1f347209b5 fix: don't show empty genre TextView if album has no genre 2025-08-07 03:00:17 +02:00
Jaime García
44f8160e21 fix: fixed incorrect album release date, hid release date TextView when there is no data 2025-08-07 03:00:10 +02:00
eddyizm
d7d25fb542 Merge pull request #20 from le-firehawk/feature-lyrics-window-fix
feat: Fix lyric scrolling during playback, keep screen on while viewing
2025-08-05 21:57:35 -07:00
Jaime García
181af686c2 feat: show sampling rate and bit depth if available 2025-08-06 01:49:30 +02:00
le-firehawk
177fc5670c feat: Fix lyric scrolling during playback, keep screen on while viewing 2025-08-05 22:44:41 +09:30
eddyizm
bfd6f28d7a chore: readme fat finger fix 2025-08-04 20:41:30 -07:00
eddyizm
549220f8f3 chore: linked new release in readme 2025-08-04 20:20:13 -07:00
387 changed files with 19568 additions and 4558 deletions

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: eddyizm
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -26,6 +26,7 @@ Outline the steps required to reproduce the bug, including any specific actions,
- Android device: [Device Model]
- Android OS version: [Android Version]
- App version: [App Version]
- App variant: [goole play services, degoogled]
- Other relevant details: [e.g., specific network conditions, external dependencies]
## Logs or Screenshots

View File

@@ -23,10 +23,11 @@ Please provide the steps to reproduce the crash:
- Android device: [Device Model]
- Android OS version: [Android Version]
- App version: [App Version]
- App variant: [goole play services, degoogled]
- Other relevant details: [e.g., specific network conditions, external dependencies]
## Crash Logs/Stack trace
<!-- If available, please provide the crash log or stack trace related to the crash. Include it inside a code block (surround with triple backticks ```). Please use the unsigned apk (app-tempo-debug.apk), as the logs would be illegible and therefore useless for this purpose. -->
<!-- If available, please provide the crash log or stack trace related to the crash. Include it inside a code block (surround with triple backticks ```). Please use the unsigned apk (app-tempus-debug.apk), as the logs would be illegible and therefore useless for this purpose. -->
## Screenshots
<!-- If applicable, add screenshots to help explain the problem. -->

View File

@@ -35,15 +35,21 @@ jobs:
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- name: Build APK
- name: Build All Release APKs
id: build
run: bash ./gradlew assembleTempoRelease
run: |
# Only build release variants (removed debug builds)
bash ./gradlew assembleTempusRelease
bash ./gradlew assembleDegoogledRelease
- name: Sign APK
id: sign_apk
- name: Create Artifact Staging Directory
run: mkdir -p release-artifacts
- name: Sign Tempus Release APKs
id: sign_tempus_release
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/tempo/release
releaseDirectory: app/build/outputs/apk/tempus/release
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -51,28 +57,73 @@ jobs:
env:
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Make artifact
uses: actions/upload-artifact@v4
- name: Prepare Signed Tempus APKs for Release
run: |
TEMPUS_PATH=app/build/outputs/apk/tempus/release
echo "--- Tempus Files BEFORE Move ---"
ls -la $TEMPUS_PATH
echo "--------------------------------"
# FIX: Use find/xargs for robust file matching and moving.
# Renaming 64-bit APK and moving to safe staging directory
find $TEMPUS_PATH -name '*arm64-v8a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
mv ./release-artifacts/*arm64-v8a*signed.apk ./release-artifacts/app-tempus-arm64-v8a-release.apk
# Renaming 32-bit APK and moving to safe staging directory
find $TEMPUS_PATH -name '*armeabi-v7a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
mv ./release-artifacts/*armeabi-v7a*signed.apk ./release-artifacts/app-tempus-armeabi-v7a-release.apk
echo "Prepared Tempus APKs."
- name: Sign Degoogled Release APKs
id: sign_degoogled_release
uses: r0adkll/sign-android-release@v1
with:
name: app-release-signed
path: ${{steps.sign_apk.outputs.signedReleaseFile}}
releaseDirectory: app/build/outputs/apk/degoogled/release
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }}
env:
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Prepare Signed Degoogled APKs for Release
run: |
DEGOOGLED_PATH=app/build/outputs/apk/degoogled/release
echo "--- Degoogled Files BEFORE Move ---"
ls -la $DEGOOGLED_PATH
echo "--------------------------------"
# FIX: Use find/xargs for robust file matching and moving.
# Renaming 64-bit APK and moving to safe staging directory
find $DEGOOGLED_PATH -name '*arm64-v8a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
mv ./release-artifacts/*arm64-v8a*signed.apk ./release-artifacts/app-degoogled-arm64-v8a-release.apk
# Renaming 32-bit APK and moving to safe staging directory
find $DEGOOGLED_PATH -name '*armeabi-v7a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
mv ./release-artifacts/*armeabi-v7a*signed.apk ./release-artifacts/app-degoogled-armeabi-v7a-release.apk
echo "Prepared Degoogled APKs."
ls -la ./release-artifacts/
- name: Create Release
id: create_release
uses: actions/create-release@v1
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref }}
release_name: Release v${{ github.ref }}
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body: '> Changelog coming soon'
env:
GITHUB_TOKEN: ${{ github.token }}
draft: false
prerelease: false
files: ./release-artifacts/*.apk
- name: Upload APK
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Upload Release APKs as artifacts (For easy pipeline access)
uses: actions/upload-artifact@v4
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}}
asset_name: app-tempo-release.apk
asset_content_type: application/zip
name: release-apks
path: ./release-artifacts/*.apk
retention-days: 30

6
.gitignore vendored
View File

@@ -14,4 +14,8 @@
.cxx
/.idea/
.env
.vscode/settings.json
.vscode/settings.json
# release / debug files
tempus-release-key.jks
app/tempus/
app/degoogled/

324
CHANGELOG.md Normal file
View File

@@ -0,0 +1,324 @@
# Changelog
## [4.6.3](https://github.com/eddyizm/tempo/releases/tag/v4.6.3) (2026-01-10)
* fix: give user feedback when trying to add podcast/radio on unsupport… by @eddyizm in https://github.com/eddyizm/tempus/pull/328
* docs: Clarify Android Auto enablement by @Forage in https://github.com/eddyizm/tempus/pull/336
* fix: instant mix gets a big refactor, with cascading fallbacks to produce a larger queue by @eddyizm in https://github.com/eddyizm/tempus/pull/330
* chore(i18n): add missing keys, update Chinese translation and alphabetize by @hongwei1203 in https://github.com/eddyizm/tempus/pull/332
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/339
* feat: Ability to toggle visibility of artist biography by @kmarius in https://github.com/eddyizm/tempus/pull/338
## [4.6.0](https://github.com/eddyizm/tempo/releases/tag/v4.6.0) (2025-12-22)
* chore: Update description_empty_title in English and Polish by @tyren234 in https://github.com/eddyizm/tempus/pull/307
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/310
* fix: checks preference and writes files externally, updates the ui by @eddyizm in https://github.com/eddyizm/tempus/pull/312
* chore: Update description_empty_title in Italian by @pochopsp in https://github.com/eddyizm/tempus/pull/314
* chore: Update description_empty_title in French and Spanish by @pochopsp in https://github.com/eddyizm/tempus/pull/315
* feat: added regular playlist to home view by @eddyizm in https://github.com/eddyizm/tempus/pull/322
## New Contributors
* @tyren234 made their first contribution in https://github.com/eddyizm/tempus/pull/307
* @pochopsp made their first contribution in https://github.com/eddyizm/tempus/pull/314
## [4.5.0](https://github.com/eddyizm/tempo/releases/tag/v4.5.0) (2025-12-12)
## What's Changed
* fix: updates starred syncing downloads to user defined directory by @eddyizm in https://github.com/eddyizm/tempus/pull/298
* fix: handle empty albums and null mappings by @eddyizm in https://github.com/eddyizm/tempus/pull/301
* feat: integrate sort recent searches chronologically by @J4mm3ris in https://github.com/eddyizm/tempus/pull/300
* feat: add heart to artist/album pages, fixed artist cover art failing by @eddyizm in https://github.com/eddyizm/tempus/pull/303
## New Contributors
* @J4mm3ris made their first contribution in https://github.com/eddyizm/tempus/pull/300
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.4.0...v4.5.0
## [4.4.0](https://github.com/eddyizm/tempo/releases/tag/v4.4.0) (2025-11-29)
## What's Changed
* chore: bringing in media service refactor previously reverted after more testing by @eddyizm in https://github.com/eddyizm/tempus/pull/286
* fix: refactor start queue to put the db writing in the background to address instant mix bug by @eddyizm in https://github.com/eddyizm/tempus/pull/287
* Feat: playerqueue fab allows playqueue actions -> saving to playlist, download all, load queue, shuffle, clean queue by @eddyizm in https://github.com/eddyizm/tempus/pull/288
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/291
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.3.0...v4.4.0
## [4.3.0](https://github.com/eddyizm/tempo/releases/tag/v4.3.0) (2025-11-23)
## What's Changed
* chore: Add Obtainium badge to README by @mikaeldui in https://github.com/eddyizm/tempus/pull/280
* fix: Revert "refactor MediaService" by @eddyizm in https://github.com/eddyizm/tempus/pull/282
* feat: add play functionality to library folder/index items by @antebudimir in https://github.com/eddyizm/tempus/pull/276
* fix: start queue blocking UI by @eddyizm in https://github.com/eddyizm/tempus/pull/283
## New Contributors
* @mikaeldui made their first contribution in https://github.com/eddyizm/tempus/pull/280
* @antebudimir made their first contribution in https://github.com/eddyizm/tempus/pull/276
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.2.6...v4.3.0
## [4.2.6](https://github.com/eddyizm/tempo/releases/tag/v4.2.6) (2025-11-22)
## What's Changed
* fix: Fix player queue soft-lock by @shrapnelnet in https://github.com/eddyizm/tempus/pull/266
* chore: Add Catalan i18n by @marcriera in https://github.com/eddyizm/tempus/pull/268
* chore: Refactor MediaService by @pca006132 in https://github.com/eddyizm/tempus/pull/267
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/272
* chore(i18n): Update Italian translation by @66Bunz in https://github.com/eddyizm/tempus/pull/278
## New Contributors
* @marcriera made their first contribution in https://github.com/eddyizm/tempus/pull/268
* @66Bunz made their first contribution in https://github.com/eddyizm/tempus/pull/278
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.2.4...v4.2.6
## [4.2.4](https://github.com/eddyizm/tempo/releases/tag/v4.2.4) (2025-11-15)
## What's Changed
* chore: Update russian strings.xml by @Sevinfolds in https://github.com/eddyizm/tempus/pull/249
* fix: disallow duplicate songs in queue by @eddyizm in https://github.com/eddyizm/tempus/pull/252
* fix:github release check by @eddyizm in https://github.com/eddyizm/tempus/pull/253
* fix: Fixed crash when viewing share by @drakeerv in https://github.com/eddyizm/tempus/pull/255
* chore: Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/257
* fix: add podcast/radio channel visible when empty podcasts/radio by @eddyizm in https://github.com/eddyizm/tempus/pull/260
## New Contributors
* @Sevinfolds made their first contribution in https://github.com/eddyizm/tempus/pull/249
* @drakeerv made their first contribution in https://github.com/eddyizm/tempus/pull/255
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.2.0...v4.2.4
## [4.2.0](https://github.com/eddyizm/tempo/releases/tag/v4.2.0) (2025-11-09)
## What's Changed
* fix: Equalizer fix in main build variant by @jaime-grj in https://github.com/eddyizm/tempus/pull/239
* fix: Images not filling holder by @eddyizm in https://github.com/eddyizm/tempus/pull/244
* feat: Make artist and album clickable by @eddyizm in https://github.com/eddyizm/tempus/pull/243
* feat: implement scroll to currently playing feature by @shrapnelnet in https://github.com/eddyizm/tempus/pull/247
* fix: shuffling genres only queuing 25 songs by @shrapnelnet in https://github.com/eddyizm/tempus/pull/246
## New Contributors
* @shrapnelnet made their first contribution in https://github.com/eddyizm/tempus/pull/247
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.3...v4.2.0
## [4.1.3](https://github.com/eddyizm/tempo/releases/tag/v4.1.3) (2025-11-06)
## What's Changed
* [fix: equalizer missing referenced value](https://github.com/eddyizm/tempus/commit/923cfd5bc97ed7db28c90348e3619d0a784fc434)
* Fix: Album track list bug by @eddyizm in https://github.com/eddyizm/tempus/pull/237
* fix: Add listener to enable equalizer when audioSessionId changes by @jaime-grj in https://github.com/eddyizm/tempus/pull/235
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.0...v4.1.3
## [4.1.0](https://github.com/eddyizm/tempo/releases/tag/v4.1.0) (2025-11-05)
## What's Changed
* chore(i18n): Update Spanish (es-ES) translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/205
* shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207
* Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220
* feat: sort artists by album count by @pca006132 in https://github.com/eddyizm/tempus/pull/206
* Fix downloaded tab performance by @pca006132 in https://github.com/eddyizm/tempus/pull/210
* fix: remove NestedScrollViews for fragment_album_page by @pca006132 in https://github.com/eddyizm/tempus/pull/216
* fix: playlist page should not snap by @pca006132 in https://github.com/eddyizm/tempus/pull/218
* fix: do not override getItemViewType and getItemId by @pca006132 in https://github.com/eddyizm/tempus/pull/221
* chore: update media3 dependencies by @pca006132 in https://github.com/eddyizm/tempus/pull/217
* fix: update MediaItems after network change by @pca006132 in https://github.com/eddyizm/tempus/pull/222
* fix: skip mapping downloaded item by @pca006132 in https://github.com/eddyizm/tempus/pull/228
## New Contributors
* @pca006132 made their first contribution in https://github.com/eddyizm/tempus/pull/207
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.7...v4.1.0
## [4.0.7](https://github.com/eddyizm/tempo/releases/tag/v4.0.7) (2025-10-28)
## What's Changed
* chore: updated tempo references to tempus including github check by @eddyizm in https://github.com/eddyizm/tempus/pull/197
* fix: Crash on share no expiration date or field returned from api by @eddyizm in https://github.com/eddyizm/tempus/pull/199
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.6...v4.0.7
## [4.0.6](https://github.com/eddyizm/tempo/releases/tag/v4.0.6) (2025-10-26)
## Attention
This release will not update previous installs as it is considered a new app, no longer `Tempo`, new icon, new app id, and new app name. Hoping it will not be a huge inconvenience but was necessary in order to publish to app stores like IzzyDroid and FDroid.
**Android Auto**
Support should be the same as before, however, I was not able to test any of the icons/visuals, so please let me know if there are any remnants of the tempo logo/icon as I believe I removed them all and replaced them successfully.
## What's Changed
* Check also underlying transport by @zc-devs in https://github.com/eddyizm/tempus/pull/90
* fix: updated workflow for 32/64 bit apks by @eddyizm in https://github.com/eddyizm/tempus/pull/176
* Unhide genre from album details view by @sebaFlame in https://github.com/eddyizm/tempus/pull/161
* fix: persist album sorting on resume by @eddyizm in https://github.com/eddyizm/tempus/pull/181
* chore: update readme and usage references to tempus. added new banner… by @eddyizm in https://github.com/eddyizm/tempus/pull/182
* Tempus rebrand by @eddyizm in https://github.com/eddyizm/tempus/pull/183
* Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/188
## New Contributors
* @zc-devs made their first contribution in https://github.com/eddyizm/tempus/pull/90
* @sebaFlame made their first contribution in https://github.com/eddyizm/tempus/pull/161
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v3.17.14...v4.0.1
## [3.17.14](https://github.com/eddyizm/tempo/releases/tag/v3.17.14) (2025-10-16)
## What's Changed
* fix: General build warning and playback issues by @le-firehawk in https://github.com/eddyizm/tempo/pull/167
* fix: persist album sort preference by @eddyizm in https://github.com/eddyizm/tempo/pull/168
* Fix album parse empty date field by @eddyizm in https://github.com/eddyizm/tempo/pull/171
* fix: Include shuffle/repeat controls in f-droid build's media notific… by @le-firehawk in https://github.com/eddyizm/tempo/pull/174
* fix: limits image size to prevent widget crash #172 by @eddyizm in https://github.com/eddyizm/tempo/pull/175
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.17.0...v3.17.14
## [3.17.0](https://github.com/eddyizm/tempo/releases/tag/v3.17.0) (2025-10-10)
## What's Changed
* chore: adding screenshot and docs for 4 icons/buttons in player control by @eddyizm in https://github.com/eddyizm/tempo/pull/162
* Update Polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/160
* feat: Make all objects in Tempo references for quick access by @le-firehawk in https://github.com/eddyizm/tempo/pull/158
* fix: Glide module incorrectly encoding IPv6 addresses by @le-firehawk in https://github.com/eddyizm/tempo/pull/159
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.6...v3.17.0
## [3.16.6](https://github.com/eddyizm/tempo/releases/tag/v3.16.6) (2025-10-08)
## What's Changed
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempo/pull/151
* fix: Re-add new equalizer settings that got lost by @jaime-grj in https://github.com/eddyizm/tempo/pull/153
* chore: removed play variant by @eddyizm in https://github.com/eddyizm/tempo/pull/155
* fix: updating release workflow to account for the 32/64 bit builds an… by @eddyizm in https://github.com/eddyizm/tempo/pull/156
* feat: Show sampling rate and bit depth in downloads by @jaime-grj in https://github.com/eddyizm/tempo/pull/154
* fix: Replace hardcoded strings in SettingsFragment by @jaime-grj in https://github.com/eddyizm/tempo/pull/152
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.0...v3.16.6
## [3.16.0](https://github.com/eddyizm/tempo/releases/tag/v3.16.0) (2025-10-07)
## What's Changed
* chore: add sha256 fingerprint for validation by @eddyizm in https://github.com/eddyizm/tempo/commit/3c58e6fbb2157a804853259dfadbbffe3b6793b5
* fix: Prevent crash when getting artist radio and song list is null by @jaime-grj in https://github.com/eddyizm/tempo/pull/117
* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/125
* fix: Update search query validation to require at least 2 characters instead of 3 by @jaime-grj in https://github.com/eddyizm/tempo/pull/124
* feat: download starred artists. by @eddyizm in https://github.com/eddyizm/tempo/pull/137
* feat: Enable downloading of song lyrics for offline viewing by @le-firehawk in https://github.com/eddyizm/tempo/pull/99
* fix: Lag during startup when local url is not available by @SinTan1729 in https://github.com/eddyizm/tempo/pull/110
* chore: add link to discussion page in settings by @eddyizm in https://github.com/eddyizm/tempo/pull/143
* feat: Notification heart rating by @eddyizm in https://github.com/eddyizm/tempo/pull/140
* chore: Unify and update polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/146
* chore: added sha256 signing key for verification by @eddyizm in https://github.com/eddyizm/tempo/pull/147
* feat: Support user-defined download directory for media by @le-firehawk in https://github.com/eddyizm/tempo/pull/21
* feat: Added support for skipping duplicates by @SinTan1729 in https://github.com/eddyizm/tempo/pull/135
* feat: Add home screen music playback widget and some updates in Turkish localization by @mucahit-kaya in https://github.com/eddyizm/tempo/pull/98
## New Contributors
* @SinTan1729 made their first contribution in https://github.com/eddyizm/tempo/pull/110
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.15.0...v3.16.0
## [3.15.0](https://github.com/eddyizm/tempo/releases/tag/v3.15.0) (2025-09-23)
## What's Changed
* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/84
* chore: Update RU locale by @ArchiDevil in https://github.com/eddyizm/tempo/pull/87
* chore: Update Korean translations by @kongwoojin in https://github.com/eddyizm/tempo/pull/97
* fix: only plays the first song on an album by @eddyizm in https://github.com/eddyizm/tempo/pull/81
* fix: handle null and not crash when disconnecting chromecast by @eddyizm in https://github.com/eddyizm/tempo/pull/81
* feat: Built-in audio equalizer by @jaime-grj in https://github.com/eddyizm/tempo/pull/94
* fix: Resolve playback issues with live radio MPEG & HLS streams by @jaime-grj in https://github.com/eddyizm/tempo/pull/89
* chore: Updates to polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/105
* feat: added 32bit build and debug build for testing. Removed unused f… by @eddyizm in https://github.com/eddyizm/tempo/pull/108
* feat: Mark currently playing song with play/pause button by @jaime-grj in https://github.com/eddyizm/tempo/pull/107
* fix: add listener to track playlist click/change by @eddyizm in https://github.com/eddyizm/tempo/pull/113
* feat: Tap anywhere on the song item to toggle playback by @jaime-grj in https://github.com/eddyizm/tempo/pull/112
## New Contributors
* @ArchiDevil made their first contribution in https://github.com/eddyizm/tempo/pull/87
* @kongwoojin made their first contribution in https://github.com/eddyizm/tempo/pull/97
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.14.8...v3.15.0
## [3.14.8](https://github.com/eddyizm/tempo/releases/tag/v3.14.8) (2025-08-30)
## What's Changed
* fix: Use correct SearchView widget to avoid crash in AlbumListPageFragment by @jaime-grj in https://github.com/eddyizm/tempo/pull/76
* chore(i18n): Update Spanish (es-ES) and English translations by @jaime-grj in https://github.com/eddyizm/tempo/pull/77
* style: Center subtitle text in empty_download_layout in fragment_download.xml when there is more than one line by @jaime-grj in https://github.com/eddyizm/tempo/pull/78
* fix: Disable "sync starred tracks/albums" switches when Cancel is clicked in warning dialog, use proper view for "Sync starred albums" dialog by @jaime-grj in https://github.com/eddyizm/tempo/pull/79
* bug fixes, chores, docs v3.14.8 by @eddyizm in https://github.com/eddyizm/tempo/pull/80
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.14.1...v3.14.8
## [3.14.1](https://github.com/eddyizm/tempo/releases/tag/v3.14.1) (2025-08-30)
## What's Changed
* feat: rating dialog added to album page by @eddyizm in https://github.com/eddyizm/tempo/pull/52
* style: Add song rating bar in landscape player controller layout by @jaime-grj in https://github.com/eddyizm/tempo/pull/57
* feat: setting to show/hide 5 star rating on playerview by @eddyizm in https://github.com/eddyizm/tempo/pull/59
* chore: setting-to-hide-song-rating by @eddyizm in https://github.com/eddyizm/tempo/pull/60
* fix: catches null value and prepares bundle appropriately adding sing… by @eddyizm in https://github.com/eddyizm/tempo/pull/64
* fix: artist filtering in library view browse artist resolves #45 by @eddyizm in https://github.com/eddyizm/tempo/pull/69
* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/70
* feat: adds sync starred albums functionality #66 by @eddyizm in https://github.com/eddyizm/tempo/pull/73
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.13.0...v3.14.1
## [3.13.0](https://github.com/eddyizm/tempo/releases/tag/v3.13.0) (2025-08-23)
## What's Changed
* style: Change position and size of rating container by @jaime-grj in https://github.com/eddyizm/tempo/pull/44
* feat: Add Turkish localization (values-tr) by @mucahit-kaya in https://github.com/eddyizm/tempo/pull/50
* chore: adding a note/not fully baked label to the sync user play queue setting by @eddyizm in https://github.com/eddyizm/tempo/commit/8ed0a4642bd0cd637c65e3115142596331fa7ef7
* fix: moved hardcoded italian save text to string template, updated with english and italian language xmls by @eddyizm in https://github.com/eddyizm/tempo/commit/26a5fb029a07752c9c0db0d08a89afd638772579
## New Contributors
* @mucahit-kaya made their first contribution in https://github.com/eddyizm/tempo/pull/50
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.12.0...v3.13.0
## [3.12.0](https://github.com/eddyizm/tempo/releases/tag/v3.12.0) (2025-08-15)
### What's Changed
* [chore]: add German translations for track info and home section strings (#29) by @BreadWare92 in https://github.com/eddyizm/tempo/pull/31
* [chore]: increased "Offline mode" text size, changed its color in dark theme by @jaime-grj in https://github.com/eddyizm/tempo/pull/33
* [chore]: Translations for sections by @skajmer in https://github.com/eddyizm/tempo/pull/30
* [chore]: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/36
* [fix]: Show placeholder string in TrackInfoDialog fields when there is no data by @jaime-grj in https://github.com/eddyizm/tempo/pull/37
* [feat]: added transcoding codec and bitrate info to PlayerControllerFragment, replace hardcoded strings by @jaime-grj in https://github.com/eddyizm/tempo/pull/38
* [chore]: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/39
* [feat]: show rating on song view by @eddyizm in https://github.com/eddyizm/tempo/pull/40
### New Contributors
* @BreadWare92 made their first contribution in https://github.com/eddyizm/tempo/pull/31
* @skajmer made their first contribution in https://github.com/eddyizm/tempo/pull/30
* @benoit-smith made their first contribution in https://github.com/eddyizm/tempo/pull/36
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.11.2...v3.12.0
## [3.11.2](https://github.com/eddyizm/tempo/releases/tag/v3.11.2) (2025-08-09)
([Full Changelog](https://github.com/eddyizm/tempo/compare/v3.10.0...eddyizm:tempo:v3.11.2?expand=1))
**Housekeeping:**
- [Chore] Added change log.
**Merged pull requests:**
- [Fix] make hardcoded strings in home fragment dynamic [\#27](https://github.com/eddyizm/tempo/pull/22) ([jaime-grj](https://github.com/jaime-grj))
- [Fix] show "System default" language option, sort languages alphabetically, include country when showing language in settings [\#26](https://github.com/eddyizm/tempo/pull/26) ([jaime-grj ](https://github.com/jaime-grj))
- [Fix] check for IP connectivity instead of Internet access [\#25](https://github.com/eddyizm/tempo/pull/25) ([jaime-grj](https://github.com/jaime-grj))
- [Fix] hide unnecessary TextViews in AlbumPageFragment when there is no data, fixed incorrect album release date [\#24](https://github.com/eddyizm/tempo/pull/24) ([jaime-grj](https://github.com/jaime-grj))
- [Feat] show sampling rate and bit depth if available [\#22](https://github.com/eddyizm/tempo/pull/22) ([jaime-grj](https://github.com/jaime-grj))
- [Feat] Fix lyric scrolling during playback, keep screen on while viewing [\#20](https://github.com/eddyizm/tempo/pull/20) ([le-firehawk](https://github.com/le-firehawk))
## [3.10.0](https://github.com/eddyizm/tempo/releases/tag/v3.10.0) (2025-08-04)
**Merged pull requests:**
- [Fix] redirection to artist fragment on artist label click [\#379](https://github.com/CappielloAntonio/tempo/pull/379)
- [Fix] Player queue lag, limits [\#385](https://github.com/CappielloAntonio/tempo/pull/385)
- [Fix] crash when sorting albums with a null artist [\#389](https://github.com/CappielloAntonio/tempo/pull/389)
- [Feat] Display toast message after adding a song to a playlist [\#371](https://github.com/CappielloAntonio/tempo/pull/371)
- [Feat] Album add to playlist context menu item [\#367](https://github.com/CappielloAntonio/tempo/pull/367)
- [Feat] Store and retrieve replay and shuffle states in preferences [\#397](https://github.com/CappielloAntonio/tempo/pull/397)
- [Feat] Enhance Android media player notification window #400
[\#400](https://github.com/CappielloAntonio/tempo/pull/400)
- [Chore] Spanish translation [\#374](https://github.com/CappielloAntonio/tempo/pull/374)
- [Chore] Polish translation [\#378](https://github.com/CappielloAntonio/tempo/pull/378)
***This log is for this fork to detail updates since 3.9.0 from the main repo.***

141
README.md
View File

@@ -1,71 +1,77 @@
<p align="center">
<img alt="Tempo" title="Tempo" src="mockup/svg/horizontal_logo.svg" width="250">
<img alt="Tempus" title="Tempus" src="mockup/svg/tempus_horizontal_logo.png" width="250">
</p>
---
<p align="center">
<b>Access your music library on all your android devices</b>
</p>
<div align="center">
<!-- Reproducible build -->
<a href="https://shields.rbtlog.dev/com.eddyizm.degoogled.tempus"><img src="https://shields.rbtlog.dev/simple/com.eddyizm.degoogled.tempus" alt="RB Status"></a>
</div>
<p align="center">
<a href="https://github.com/eddyizm/tempo/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
<a href="https://github.com/eddyizm/tempus/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.eddyizm.degoogled.tempus"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.eddyizm.tempus%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Feddyizm%2Ftempus%22%2C%22author%22%3A%22eddyizm%22%2C%22name%22%3A%22Tempus%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22tempus%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22overrideSource%22%3A%22GitHub%22%7D"><img width="200" src="https://github.com/user-attachments/assets/119e7ff4-2636-43cb-ab7f-1b6a58ac3570" /></a>
</p>
<!-- <p align="center">
<!--
<a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a>
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.cappielloantonio.tempo"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
</p> -->
-->
**Tempo** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device.
**Tempus** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device.
Tempo does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Last.fm to personalize your music experience.
Tempus does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Listenbrainz.org and Last.fm to personalize your music experience (These must be supported by your backend).
**If you find Tempo useful, please consider starring the project on GitHub. It would mean a lot to me and help promote the app to a wider audience.**
The project is a fork of [Tempo](#credits).
**If you find Tempus useful, please consider starring the project on GitHub. It would mean a lot to me and help promote the app to a wider audience.**
**Use the Github version of the app for full Android Auto and Chromecast support.**
## Fork
sha256 signing key fingerprint
`B7:85:01:B9:34:D0:4E:0A:CA:8D:94:AF:D6:72:6A:4D:1D:CE:65:79:7F:1D:41:71:0F:64:3C:29:00:EB:1D:1D`
This fork is my attempt to keep development moving forward and merge in PR's that have been sitting for a while in the main repo. Thankful to @CappielloAntonio for the amazing app and hopefully we can continue to build on top of it. I will only be releasing on github and if I am not able to merge back to the main repo, I plan to rename the app to be able to publish it to fdroid and possibly google play? We will see.
### Releases
v3.10.0 applies the following PR's (fix/feat/chore):
fix: [379](https://github.com/CappielloAntonio/tempo/pull/379) -Fix: redirection to artist fragment on artist label click
fix: [385](https://github.com/CappielloAntonio/tempo/pull/385) -Player queue lag, limits
fix: [389](https://github.com/CappielloAntonio/tempo/pull/389) -Fix crash when sorting albums with a null artist
feat: [371](https://github.com/CappielloAntonio/tempo/pull/371) -Display toast message after adding a song to a playlist
feat: [367](https://github.com/CappielloAntonio/tempo/pull/367) -Album add to playlist context menu item
chore: [374](https://github.com/CappielloAntonio/tempo/pull/374) -Spanish translation
feat: [397](https://github.com/CappielloAntonio/tempo/pull/397) -Store and retrieve replay and shuffle states in preferences
feat:[400](https://github.com/CappielloAntonio/tempo/pull/400) - enhance Android media player notification window
chore: [378](https://github.com/CappielloAntonio/tempo/pull/378) Polish translation
Please note the two variants in the release assets include release/debug and 32/64 bit flavors.
`app-tempus` <- The github release with all the android auto/chromecast features
`app-degoogled*` <- The izzyOnDroid release that goes without any of the google stuff. It is now available on izzyOnDroid (64bit) I am releasing the both 32/64bit apk's here on github for those who need a 32bit version.
[CHANGELOG.md](CHANGELOG.md)
## Usage
[Documentation](USAGE.md) (work in progress)
## Features
- **Subsonic Integration**: Tempo seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
- **Subsonic Integration**: Tempus seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
- **Sleek and Intuitive UI**: Enjoy a clean and user-friendly interface designed to enhance your music listening experience, tailored to your preferences and listening history.
- **Browse and Search**: Easily navigate through your music library using various browsing and searching options, including artists, albums, genres, playlists, decades and more.
- **Streaming and Offline Mode**: Stream music directly from your Subsonic server. Offline mode is currently under active development and may have limitations when using multiple servers.
- **Playlist Management**: Create, edit, and manage playlists to curate your perfect music collection.
- **Gapless Playback**: Experience uninterrupted playback with gapless listening mode.
- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state.
- **Scrobbling Integration**: Optionally integrate Tempo with Last.fm to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempo, expanding your audio entertainment options.
- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state.*
- **Scrobbling Integration**: Optionally integrate Tempus with Last.fm or Listenbrainz.org to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempus, expanding your audio entertainment options.
- **Instant Mix**: Full refactor of instant mix function which leverages subsonics similar songs by artist/album and randomSongs endpoints to server a larger play queue more reliably.
- **Transcoding Support**: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server.
- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.
<p align="center">
<img src="mockup/feat/1_screenshot.png" width=200>
<img src="mockup/feat/2_screenshot.png" width=200>
<img src="mockup/feat/3_screenshot.png" width=200>
<img src="mockup/feat/4_screenshot.png" width=200>
<img src="mockup/feat/5_screenshot.png" width=200>
<img src="mockup/feat/6_screenshot.png" width=200>
<img src="mockup/feat/7_scregetRecentSearchSuggestionenshot.png" width=200>
<img src="mockup/feat/8_screenshot.png" width=200>
</p>
## Sponsors
Tempo is an open-source project developed and maintained solely by me. I would like to express my heartfelt thanks to all the users who have shown their love and support for Tempo. Your contributions and encouragement mean a lot to me, and they help drive the development and improvement of the app.
If you would like to sponsor the project and show your support, you can make a donation or contribution by visiting the [**sponsorship page**](https://www.buymeacoffee.com/a.cappiello). Your generosity will help cover the costs of development and further enhancements.
- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.*
- **Multiple Libraries**: Tempus handles multi-library setups gracefully. They are displayed as Library folders.
- **Equalizer**: Option to use in app equalizer.
- **Widget**: New widget to keeping the basic controls on your screen at all times.
- **Available in 11 languages**: Currently in Chinese, French, German, Italian, Korean, Polish, Portuguese, Russion, Spanish and Turkish
**Github version only*
## Screenshot
<p align="center">
@@ -73,14 +79,13 @@ If you would like to sponsor the project and show your support, you can make a d
</p>
<p align="center">
<img src="mockup/light/1_screenshot.png" width=200>
<img src="mockup/light/2_screenshot.png" width=200>
<img src="mockup/light/3_screenshot.png" width=200>
<img src="mockup/light/4_screenshot.png" width=200>
<img src="mockup/light/5_screenshot.png" width=200>
<img src="mockup/light/6_screenshot.png" width=200>
<img src="mockup/light/7_screenshot.png" width=200>
<img src="mockup/light/8_screenshot.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_light.png" width=200>
</p>
<br>
@@ -90,16 +95,36 @@ If you would like to sponsor the project and show your support, you can make a d
</p>
<p align="center">
<img src="mockup/dark/1_screenshot.png" width=200>
<img src="mockup/dark/2_screenshot.png" width=200>
<img src="mockup/dark/3_screenshot.png" width=200>
<img src="mockup/dark/4_screenshot.png" width=200>
<img src="mockup/dark/5_screenshot.png" width=200>
<img src="mockup/dark/6_screenshot.png" width=200>
<img src="mockup/dark/7_screenshot.png" width=200>
<img src="mockup/dark/8_screenshot.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_dark.png" width=200>
</p>
## Contributing
Please fork and open PR's against the development branch. Make sure your PR builds successfully.
If there is an UI change, please include a before/after screenshot and a short video/gif if that helps elaborating the fix/feature in the PR.
Currently there are no tests but I would love to start on some unit tests.
Not a hard requirement but any new feature/change should ideally include an update to the nacent documention.
## Support
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
## License
Tempo is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome.
Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome.
## Credits
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
[Opensvg.org](https://opensvg.org) for the new turntable logo.

214
USAGE.md Normal file
View File

@@ -0,0 +1,214 @@
# Tempus Usage Guide
[<- back home](README.md)
## Table of Contents
- [Prerequisites](#prerequisites)
- [Getting Started](#getting-started)
- [Server Configuration](#server-configuration)
- [Main Features](#main-features)
- [Navigation](#navigation)
- [Playback Controls](#playback-controls)
- [Favorites](#favorites)
- [Playlist Management](#playlist-management)
- [Android Auto](#android-auto)
- [Settings](#settings)
- [Troubleshooting](#troubleshooting)
## Prerequisites
**Important Notice**: This app is a Subsonic-compatible client and does not provide any music content itself. To use this application, you must have:
- An active Subsonic API server (or compatible service) already set up
- Valid login credentials for your Subsonic server
- Music content uploaded and organized on your server
### Verified backends
This app works with any service that implements the Subsonic API, including:
- [LMS - Lightweight Music Server](https://github.com/epoupon/lms) - *personal fave and my backend*
- [Navidrome](https://www.navidrome.org/)
- [Gonic](https://github.com/sentriz/gonic)
- [Ampache](https://github.com/ampache/ampache)
- [NextCloud Music](https://apps.nextcloud.com/apps/music)
- [Airsonic Advanced](https://github.com/kagemomiji/airsonic-advanced)
## Getting Started
### Installation
1. Download the APK from the [Releases](https://github.com/eddyizm/tempus/releases) section
2. Enable "Install from unknown sources" in your Android settings
3. Install the application
### First Launch
1. Open the application
2. You will be prompted to configure your server connection
3. Grant necessary permissions for media playback and background operation
## Server Configuration
### Initial Setup
**IN PROGRESS**
1. Enter your server URL (e.g., `https://your-subsonic-server.com`)
2. Provide your username and password
3. Test the connection to ensure proper configuration
### Advanced Settings
**TODO**
## Main Features
### Library View
**Multi-library**
Tempus handles multi-library setups gracefully. They are displayed as Library folders.
However, if you want to limit or change libraries you could use a workaround, if your server supports it.
You can create multiple users , one for each library, and save each of them in Tempus app.
### Folder or index playback
If your Subsonic-compatible server exposes the folder tree **or** provides an artist index (for example Gonic, Navidrome, or any backend with folder browsing enabled), Tempus lets you play an entire folder from anywhere in the library hierarchy:
<p align="left">
<img src="mockup/usage/music_folders_root.png" width=317 style="margin-right:16px;">
<img src="mockup/usage/music_folders_playback.png" width=317>
</p>
- The **Library ▸ Music folders** screen shows each top-level folder with a play icon only after you drill into it. The root entry remains a simple navigator.
- When viewing **inner folders** **or artist index entries**, tap the new play button to immediately enqueue every audio track inside that folder/index and all nested subfolders.
- Video files are excluded automatically, so only playable audio ends up in the queue.
No extra config is needed—Tempus adjusts based on the connected backend.
### Now Playing Screen
On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons.
<p align="left">
<img src="mockup/usage/player_icons.png" width=159>
</p>
*marked the icons with numbers for clarity*
1. Downloads the track (there is a notification if the android screen but not a pop toast currently )
2. Adds track to playlist - pops up playlist dialog.
3. Adds tracks to the queue via instant mix function
* TBD: what is the _instant mix function_?
* Uses [getSimilarSongs](https://opensubsonic.netlify.app/docs/endpoints/getsimilarsongs/) of OpenSubsonic API.
Which tracks to be mixed depends on the server implementation. For example, Navidrome gets 15 similar artists from LastFM, then 20 top songs from each.
4. Saves play queue (if the feature is enabled in the settings)
* if the setting is not enabled, it toggles a view of the lyrics if available (slides to the right)
### Podcasts
If your server supports it - add a podcast rss feed
<p align="left">
<img src="mockup/usage/add_podcast_feed.png" width=317>
</p>
### Radio Stations
If your server supports it - add a internet radio station feed
<p align="left">
<img src="mockup/usage/add_radio_station.png" width=326>
</p>
## Navigation
### Bottom Navigation Bar
**IN PROGRESS**
- **Home**: Recently played and server recommendations
- **Library**: Your server's complete music collection
- **Download**: Locally downloaded files from server
## Playback Controls
### Streaming Controls
**TODO**
### Advanced Controls
**TODO**
## Favorites
### Favorites (aka heart aka star) to albums and artists
- Long pressing on an album gives you access to heart/unheart an album
<p align="center">
<img src="mockup/usage/fave_album.png" width=376>
</p>
- Long pressing on an artist cover gets you the same access to to heart/unheart an album
<p align="center">
<img src="mockup/usage/fave_artist.png" width=376>
</p>
## Playlist Management
### Server Playlists
**TODO**
### Creating Playlists
**TODO**
## Settings
## Android Auto
### Enabling on your head unit
To allow the Tempus app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempus isn't installed through Play Store. Note that the Android Auto developer settings are different from the global Android "Developer options".
1. Switch to developer mode in the Android Auto settings by tapping ten times on the "Version" item at the bottom, followed by giving your permission.
<p align="left">
<img width="270" height="600" alt="1a" src="https://github.com/user-attachments/assets/f09f6999-9761-4b05-8ec7-bf221a15dda3" />
<img width="270" height="600" alt="1b" src="https://github.com/user-attachments/assets/0795e508-ba01-41c5-96a7-7c03b0156591" />
<img width="270" height="600" alt="1c" src="https://github.com/user-attachments/assets/51c15f67-fddb-452e-b5d3-5092edeab390" />
</p>
2. Go to the "Developer settings" by the menu at the top right.
<p align="left">
<img width="270" height="600" alt="2" src="https://github.com/user-attachments/assets/1ecd1f3e-026d-4d25-87f2-be7f12efbac6" />
</p>
3. Scroll down to the bottom and check "Unknown sources".
<p align="left">
<img width="270" height="600" alt="3" src="https://github.com/user-attachments/assets/37db88e9-1b76-417f-9c47-da9f3a750fff" />
</p>
### Server Settings
**IN PROGRESS**
- Manage multiple server connections
- Configure sync intervals
- Set data usage limits for streaming
### Audio Settings
**IN PROGRESS**
- Streaming quality settings
- Offline caching preferences
### Appearance
**TODO**
## Troubleshooting
### Connection Issues
**TODO**
### Common Issues
**TODO**
### Support
For additional help:
- Question? Start a [Discussion](https://github.com/eddyizm/tempus/discussions)
- Open an [issue](https://github.com/eddyizm/tempus/issues) if you don't find a discussion solving your issue.
- Consult your Subsonic server's documentation
---
*Note: This app requires a pre-existing Subsonic-compatible server with music content.*

View File

@@ -10,9 +10,8 @@ android {
minSdkVersion 24
targetSdk 35
versionCode 26
versionName '3.10.0'
versionCode 12
versionName '4.6.3'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions {
@@ -23,25 +22,39 @@ android {
]
}
}
}
splits {
abi {
enable true
reset()
//noinspection ChromeOsAbiSupport
include 'armeabi-v7a', 'arm64-v8a'
universalApk false
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid)
includeInApk = false
// Disables dependency metadata when building Android App Bundles (for Google Play)
includeInBundle = false
}
flavorDimensions += "default"
productFlavors {
tempo {
tempus {
dimension = "default"
applicationId 'com.cappielloantonio.tempo'
applicationId 'com.eddyizm.tempus'
}
notquitemy {
degoogled {
dimension = "default"
applicationId "com.cappielloantonio.notquitemy.tempo"
applicationId "com.eddyizm.degoogled.tempus"
}
play {
dimension = "default"
applicationId "com.cappielloantonio.play.tempo"
}
}
buildTypes {
@@ -51,6 +64,11 @@ android {
debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix ".debug"
debuggable true
}
}
compileOptions {
@@ -92,13 +110,13 @@ dependencies {
implementation 'com.github.bumptech.glide:annotations:4.16.0'
// Media3
implementation 'androidx.media3:media3-session:1.5.1'
implementation 'androidx.media3:media3-common:1.5.1'
implementation 'androidx.media3:media3-exoplayer:1.5.1'
implementation 'androidx.media3:media3-ui:1.5.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.5.1'
tempoImplementation 'androidx.media3:media3-cast:1.5.1'
playImplementation 'androidx.media3:media3-cast:1.5.1'
implementation 'androidx.media3:media3-session:1.8.0'
implementation 'androidx.media3:media3-common:1.8.0'
implementation 'androidx.media3:media3-exoplayer:1.8.0'
implementation 'androidx.media3:media3-ui:1.8.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
tempusImplementation 'androidx.media3:media3-cast:1.8.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
@@ -112,4 +130,4 @@ java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,6 @@
package com.cappielloantonio.tempo.service
import androidx.media3.common.util.UnstableApi
@UnstableApi
class MediaService : BaseMediaService()

View File

@@ -0,0 +1,54 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.49"
android:scaleY="0.49"
android:translateX="130.56"
android:translateY="130.56">
<path
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
android:fillColor="#8CC152"/> <path
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
android:fillColor="#62A43B"/> <path
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
android:fillColor="#8CC152"/> <path
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
android:fillColor="#CCD1D9"/>
<path
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
android:fillColor="#656D78"/>
<path
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
android:fillColor="#656D78"/>
<path
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
android:fillColor="#FFCE54"/>
<path
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
android:fillColor="#F6BB42"/>
<path
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
android:fillColor="#434A54"/>
</group>
</vector>

View File

@@ -0,0 +1,53 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.55"
android:scaleY="0.55"
android:translateX="150.56"
android:translateY="150.56">
<path
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
android:fillColor="#8CC152"/> <path
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
android:fillColor="#62A43B"/> <path
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
android:fillColor="#8CC152"/> <path
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
android:fillColor="#CCD1D9"/>
<path
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
android:fillColor="#656D78"/>
<path
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
android:fillColor="#656D78"/>
<path
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
android:fillColor="#FFCE54"/>
<path
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
android:fillColor="#F6BB42"/>
<path
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
android:fillColor="#434A54"/>
</group>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#626A75</color>
</resources>

View File

@@ -42,6 +42,16 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="asset"
android:scheme="tempo" />
</intent-filter>
</activity>
<service
@@ -73,5 +83,20 @@
android:name="autoStoreLocales"
android:value="true" />
</service>
<receiver
android:name=".widget.WidgetProvider4x1"
android:exported="false"
android:label="@string/widget_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info"/>
</receiver>
</application>
</manifest>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.database.converter.DateConverters;
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
import com.cappielloantonio.tempo.database.dao.DownloadDao;
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.database.dao.LyricsDao;
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
import com.cappielloantonio.tempo.database.dao.QueueDao;
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
@@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Favorite;
import com.cappielloantonio.tempo.model.LyricsCache;
import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.model.RecentSearch;
import com.cappielloantonio.tempo.model.Server;
@@ -28,9 +30,9 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
@UnstableApi
@Database(
version = 10,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class},
autoMigrations = {@AutoMigration(from = 9, to = 10)}
version = 13,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
)
@TypeConverters({DateConverters.class})
public abstract class AppDatabase extends RoomDatabase {
@@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract SessionMediaItemDao sessionMediaItemDao();
public abstract PlaylistDao playlistDao();
public abstract LyricsDao lyricsDao();
}

View File

@@ -15,6 +15,9 @@ public interface DownloadDao {
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
LiveData<List<Download>> getAll();
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
List<Download> getAllSync();
@Query("SELECT * FROM download WHERE id = :id")
Download getOne(String id);
@@ -30,6 +33,9 @@ public interface DownloadDao {
@Query("DELETE FROM download WHERE id = :id")
void delete(String id);
@Query("DELETE FROM download WHERE id IN (:ids)")
void deleteByIds(List<String> ids);
@Query("DELETE FROM download")
void deleteAll();
}

View File

@@ -0,0 +1,24 @@
package com.cappielloantonio.tempo.database.dao;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.cappielloantonio.tempo.model.LyricsCache;
@Dao
public interface LyricsDao {
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
LyricsCache getOne(String songId);
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
LiveData<LyricsCache> observeOne(String songId);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(LyricsCache lyricsCache);
@Query("DELETE FROM lyrics_cache WHERE song_id = :songId")
void delete(String songId);
}

View File

@@ -12,9 +12,12 @@ import java.util.List;
@Dao
public interface RecentSearchDao {
@Query("SELECT * FROM recent_search ORDER BY search DESC")
@Query("SELECT search FROM recent_search ORDER BY timestamp DESC")
List<String> getRecent();
@Query("SELECT search FROM recent_search ORDER BY search DESC")
List<String> getAlpha();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(RecentSearch search);

View File

@@ -3,8 +3,8 @@ package com.cappielloantonio.tempo.github;
import com.cappielloantonio.tempo.github.api.release.ReleaseClient;
public class Github {
private static final String OWNER = "CappielloAntonio";
private static final String REPO = "Tempo";
private static final String OWNER = "eddyizm";
private static final String REPO = "Tempus";
private ReleaseClient releaseClient;
public ReleaseClient getReleaseClient() {

View File

@@ -7,10 +7,11 @@ public class UpdateUtil {
public static boolean showUpdateDialog(LatestRelease release) {
if (release.getTagName() == null) return false;
String remoteTag = release.getTagName().replaceAll("^\\D+", "");
try {
String[] local = BuildConfig.VERSION_NAME.split("\\.");
String[] remote = release.getTagName().split("\\.");
String[] remote = remoteTag.split("\\.");
for (int i = 0; i < local.length; i++) {
int localPart = Integer.parseInt(local[i]);

View File

@@ -4,14 +4,18 @@ import android.content.Context;
import androidx.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
import com.bumptech.glide.Registry;
import com.bumptech.glide.module.AppGlideModule;
import com.bumptech.glide.request.RequestOptions;
import com.cappielloantonio.tempo.util.Preferences;
import java.io.InputStream;
@GlideModule
public class CustomGlideModule extends AppGlideModule {
@Override
@@ -20,4 +24,9 @@ public class CustomGlideModule extends AppGlideModule {
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "cache", diskCacheSize));
builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
}
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.replace(String.class, InputStream.class, new IPv6StringLoader.Factory());
}
}

View File

@@ -1,6 +1,7 @@
package com.cappielloantonio.tempo.glide;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
@@ -16,6 +17,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.signature.ObjectKey;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.R;
@@ -109,9 +111,21 @@ public class CustomGlideRequest {
return uri.toString();
}
public static void loadAlbumArtBitmap(Context context,
String coverId,
int size,
CustomTarget<Bitmap> target) {
String url = createUrl(coverId, size);
Glide.with(context)
.asBitmap()
.load(url)
.apply(createRequestOptions(context, coverId, ResourceType.Album))
.into(target);
}
public static class Builder {
private final RequestManager requestManager;
private Object item;
private String item;
private Builder(Context context, String item, ResourceType type) {
this.requestManager = Glide.with(context);

View File

@@ -0,0 +1,110 @@
package com.cappielloantonio.tempo.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import com.bumptech.glide.signature.ObjectKey;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class IPv6StringLoader implements ModelLoader<String, InputStream> {
private static final int DEFAULT_TIMEOUT_MS = 2500;
@Override
public boolean handles(@NonNull String model) {
return model.startsWith("http://") || model.startsWith("https://");
}
@Override
public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
if (!handles(model)) {
return null;
}
return new LoadData<>(new ObjectKey(model), new IPv6StreamFetcher(model));
}
private static class IPv6StreamFetcher implements DataFetcher<InputStream> {
private final String model;
private InputStream stream;
private HttpURLConnection connection;
IPv6StreamFetcher(String model) {
this.model = model;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
try {
URL url = new URL(model);
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(DEFAULT_TIMEOUT_MS);
connection.setReadTimeout(DEFAULT_TIMEOUT_MS);
connection.setUseCaches(true);
connection.setDoInput(true);
connection.connect();
if (connection.getResponseCode() / 100 != 2) {
callback.onLoadFailed(new IOException("Request failed with status code: " + connection.getResponseCode()));
return;
}
stream = connection.getInputStream();
callback.onDataReady(stream);
} catch (IOException e) {
callback.onLoadFailed(e);
}
}
@Override
public void cleanup() {
if (stream != null) {
try {
stream.close();
} catch (IOException ignored) {
}
}
if (connection != null) {
connection.disconnect();
}
}
@Override
public void cancel() {
// HttpURLConnection does not provide a direct cancel mechanism.
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.REMOTE;
}
}
public static class Factory implements ModelLoaderFactory<String, InputStream> {
@NonNull
@Override
public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new IPv6StringLoader();
}
@Override
public void teardown() {
// No-op
}
}
}

View File

@@ -27,8 +27,11 @@ public interface ClickCallback {
default void onInternetRadioStationClick(Bundle bundle) {}
default void onInternetRadioStationLongClick(Bundle bundle) {}
default void onMusicFolderClick(Bundle bundle) {}
default void onMusicFolderPlay(Bundle bundle) {}
default void onMusicDirectoryClick(Bundle bundle) {}
default void onMusicDirectoryPlay(Bundle bundle) {}
default void onMusicIndexClick(Bundle bundle) {}
default void onMusicIndexPlay(Bundle bundle) {}
default void onDownloadGroupLongClick(Bundle bundle) {}
default void onShareClick(Bundle bundle) {}
default void onShareLongClick(Bundle bundle) {}

View File

@@ -8,18 +8,18 @@ import androidx.room.PrimaryKey
import com.cappielloantonio.tempo.subsonic.models.Child
import com.cappielloantonio.tempo.util.Preferences
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Keep
@Parcelize
@Entity(tableName = "chronology")
class Chronology(@PrimaryKey override val id: String) : Child(id) {
class Chronology(
@PrimaryKey override val id: String,
@ColumnInfo(name = "timestamp")
var timestamp: Long = System.currentTimeMillis()
var timestamp: Long = System.currentTimeMillis(),
@ColumnInfo(name = "server")
var server: String? = null
var server: String? = null,
) : Child(id) {
constructor(mediaItem: MediaItem) : this(mediaItem.mediaMetadata.extras!!.getString("id")!!) {
parentId = mediaItem.mediaMetadata.extras!!.getString("parentId")
isDir = mediaItem.mediaMetadata.extras!!.getBoolean("isDir")
@@ -37,6 +37,8 @@ class Chronology(@PrimaryKey override val id: String) : Child(id) {
transcodedSuffix = mediaItem.mediaMetadata.extras!!.getString("transcodedSuffix")
duration = mediaItem.mediaMetadata.extras!!.getInt("duration")
bitrate = mediaItem.mediaMetadata.extras!!.getInt("bitrate")
samplingRate = mediaItem.mediaMetadata.extras!!.getInt("samplingRate")
bitDepth = mediaItem.mediaMetadata.extras!!.getInt("bitDepth")
path = mediaItem.mediaMetadata.extras!!.getString("path")
isVideo = mediaItem.mediaMetadata.extras!!.getBoolean("isVideo")
userRating = mediaItem.mediaMetadata.extras!!.getInt("userRating")

View File

@@ -10,19 +10,17 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
@Entity(tableName = "download")
class Download(@PrimaryKey override val id: String) : Child(id) {
class Download(
@PrimaryKey override val id: String,
@ColumnInfo(name = "playlist_id")
var playlistId: String? = null
var playlistId: String? = null,
@ColumnInfo(name = "playlist_name")
var playlistName: String? = null
var playlistName: String? = null,
@ColumnInfo(name = "download_state", defaultValue = "1")
var downloadState: Int = 0
var downloadState: Int = 0,
@ColumnInfo(name = "download_uri", defaultValue = "")
var downloadUri: String? = null
var downloadUri: String? = null,
) : Child(id) {
constructor(child: Child) : this(child.id) {
parentId = child.parentId
isDir = child.isDir
@@ -40,6 +38,8 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
transcodedSuffix = child.transcodedSuffix
duration = child.duration
bitrate = child.bitrate
samplingRate = child.samplingRate
bitDepth = child.bitDepth
path = child.path
isVideo = child.isVideo
userRating = child.userRating
@@ -60,5 +60,5 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
@Keep
data class DownloadStack(
var id: String,
var view: String?
var view: String?,
)

View File

@@ -0,0 +1,25 @@
package com.cappielloantonio.tempo.model
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlin.jvm.JvmOverloads
@Keep
@Entity(tableName = "lyrics_cache")
data class LyricsCache @JvmOverloads constructor(
@PrimaryKey
@ColumnInfo(name = "song_id")
var songId: String,
@ColumnInfo(name = "artist")
var artist: String? = null,
@ColumnInfo(name = "title")
var title: String? = null,
@ColumnInfo(name = "lyrics")
var lyrics: String? = null,
@ColumnInfo(name = "structured_lyrics")
var structuredLyrics: String? = null,
@ColumnInfo(name = "updated_at")
var updatedAt: Long = System.currentTimeMillis()
)

View File

@@ -10,20 +10,18 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
@Entity(tableName = "queue")
class Queue(override val id: String) : Child(id) {
class Queue(
override val id: String,
@PrimaryKey
@ColumnInfo(name = "track_order")
var trackOrder: Int = 0
var trackOrder: Int = 0,
@ColumnInfo(name = "last_play")
var lastPlay: Long = 0
var lastPlay: Long = 0,
@ColumnInfo(name = "playing_changed")
var playingChanged: Long = 0
var playingChanged: Long = 0,
@ColumnInfo(name = "stream_id")
var streamId: String? = null
var streamId: String? = null,
) : Child(id) {
constructor(child: Child) : this(child.id) {
parentId = child.parentId
isDir = child.isDir
@@ -41,6 +39,8 @@ class Queue(override val id: String) : Child(id) {
transcodedSuffix = child.transcodedSuffix
duration = child.duration
bitrate = child.bitrate
samplingRate = child.samplingRate
bitDepth = child.bitDepth
path = child.path
isVideo = child.isVideo
userRating = child.userRating

View File

@@ -13,5 +13,8 @@ import kotlinx.parcelize.Parcelize
data class RecentSearch(
@PrimaryKey
@ColumnInfo(name = "search")
var search: String
var search: String,
@ColumnInfo(name = "timestamp", defaultValue = "0")
var timestamp: Long
) : Parcelable

View File

@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.model
import android.net.Uri
import android.os.Bundle
import androidx.annotation.Keep
import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.RequestMetadata
import androidx.media3.common.MediaMetadata
@@ -243,6 +244,13 @@ class SessionMediaItem() {
.setAlbumTitle(album)
.setArtist(artist)
.setArtworkUri(artworkUri)
.setUserRating(HeartRating(starred != null))
.setSupportedCommands(
listOf(
Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON,
Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF
)
)
.setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)

View File

@@ -3,13 +3,15 @@ package com.cappielloantonio.tempo.repository;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import android.util.Log;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.interfaces.DecadesCallback;
import com.cappielloantonio.tempo.interfaces.MediaCallback;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.AlbumInfo;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Constants.SeedType;
import java.util.ArrayList;
import java.util.Calendar;
@@ -31,14 +33,22 @@ public class AlbumRepository {
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
if (response.isSuccessful()
&& response.body() != null
&& response.body().getSubsonicResponse().getAlbumList2() != null
&& response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
listLiveAlbums.setValue(response.body().getSubsonicResponse().getAlbumList2().getAlbums());
} else {
Log.e("AlbumRepository", "API Error on getAlbums. HTTP Code: " + response.code());
listLiveAlbums.setValue(new ArrayList<>());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.e("AlbumRepository", "Network Failure on getAlbums: " + t.getMessage());
listLiveAlbums.setValue(new ArrayList<>());
}
});
@@ -195,29 +205,12 @@ public class AlbumRepository {
return albumInfo;
}
public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) {
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(album.getId(), count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
songs.addAll(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
}
callback.onLoadMedia(songs);
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
callback.onLoadMedia(new ArrayList<>());
}
});
public MutableLiveData<List<Child>> getInstantMix(AlbumID3 album, int count) {
// Delegate to the centralized SongRepository
return new SongRepository().getInstantMix(album.getId(), SeedType.ALBUM, count);
}
public MutableLiveData<List<Integer>> getDecades() {
MutableLiveData<List<Integer>> decades = new MutableLiveData<>();
@@ -228,7 +221,7 @@ public class AlbumRepository {
@Override
public void onLoadYear(int last) {
if (first != -1 && last != -1) {
List<Integer> decadeList = new ArrayList();
List<Integer> decadeList = new ArrayList<>();
int startDecade = first - (first % 10);
int lastDecade = last - (last % 10);
@@ -289,4 +282,4 @@ public class AlbumRepository {
}
});
}
}
}

View File

@@ -2,23 +2,109 @@ package com.cappielloantonio.tempo.repository;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import android.util.Log;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.interfaces.MediaCallback;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
import com.cappielloantonio.tempo.util.Constants.SeedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class ArtistRepository {
private final AlbumRepository albumRepository;
public ArtistRepository() {
this.albumRepository = new AlbumRepository();
}
public void getArtistAllSongs(String artistId, ArtistSongsCallback callback) {
Log.d("ArtistSync", "Getting albums for artist: " + artistId);
// Get the artist info first, which contains the albums
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getArtist(artistId)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null &&
response.body().getSubsonicResponse().getArtist() != null &&
response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
List<AlbumID3> albums = response.body().getSubsonicResponse().getArtist().getAlbums();
Log.d("ArtistSync", "Got albums directly: " + albums.size());
if (!albums.isEmpty()) {
fetchAllAlbumSongsWithCallback(albums, callback);
} else {
Log.d("ArtistSync", "No albums found in artist response");
callback.onSongsCollected(new ArrayList<>());
}
} else {
Log.d("ArtistSync", "Failed to get artist info");
callback.onSongsCollected(new ArrayList<>());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.d("ArtistSync", "Error getting artist info: " + t.getMessage());
callback.onSongsCollected(new ArrayList<>());
}
});
}
private void fetchAllAlbumSongsWithCallback(List<AlbumID3> albums, ArtistSongsCallback callback) {
if (albums == null || albums.isEmpty()) {
Log.d("ArtistSync", "No albums to process");
callback.onSongsCollected(new ArrayList<>());
return;
}
List<Child> allSongs = new ArrayList<>();
AtomicInteger remainingAlbums = new AtomicInteger(albums.size());
Log.d("ArtistSync", "Processing " + albums.size() + " albums");
for (AlbumID3 album : albums) {
Log.d("ArtistSync", "Getting tracks for album: " + album.getName());
MutableLiveData<List<Child>> albumTracks = albumRepository.getAlbumTracks(album.getId());
albumTracks.observeForever(songs -> {
Log.d("ArtistSync", "Got " + (songs != null ? songs.size() : 0) + " songs from album");
if (songs != null) {
allSongs.addAll(songs);
}
albumTracks.removeObservers(null);
int remaining = remainingAlbums.decrementAndGet();
Log.d("ArtistSync", "Remaining albums: " + remaining);
if (remaining == 0) {
Log.d("ArtistSync", "All albums processed. Total songs: " + allSongs.size());
callback.onSongsCollected(allSongs);
}
});
}
}
public interface ArtistSongsCallback {
void onSongsCollected(List<Child> songs);
}
public MutableLiveData<List<ArtistID3>> getStarredArtists(boolean random, int size) {
MutableLiveData<List<ArtistID3>> starredArtists = new MutableLiveData<>(new ArrayList<>());
@@ -65,7 +151,7 @@ public class ArtistRepository {
if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) {
for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) {
if(index != null && index.getArtists() != null) {
if(index.getArtists() != null) {
artists.addAll(index.getArtists());
}
}
@@ -89,7 +175,7 @@ public class ArtistRepository {
}
/*
* Metodo che mi restituisce le informazioni essenzionali dell'artista (cover, numero di album...)
* Method that returns essential artist information (cover, album number, etc.)
*/
public void getArtistInfo(List<ArtistID3> artists, MutableLiveData<List<ArtistID3>> list) {
List<ArtistID3> liveArtists = list.getValue();
@@ -203,26 +289,8 @@ public class ArtistRepository {
}
public MutableLiveData<List<Child>> getInstantMix(ArtistID3 artist, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(artist.getId(), count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return instantMix;
// Delegate to the centralized SongRepository
return new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count);
}
public MutableLiveData<List<Child>> getRandomSong(ArtistID3 artist, int count) {
@@ -230,24 +298,42 @@ public class ArtistRepository {
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getTopSongs(artist.getName(), count)
.getArtist(artist.getId())
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getTopSongs() != null && response.body().getSubsonicResponse().getTopSongs().getSongs() != null) {
List<Child> songs = response.body().getSubsonicResponse().getTopSongs().getSongs();
if (response.isSuccessful() && response.body() != null &&
response.body().getSubsonicResponse().getArtist() != null &&
response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
if (songs != null && !songs.isEmpty()) {
Collections.shuffle(songs);
List<AlbumID3> albums = response.body().getSubsonicResponse().getArtist().getAlbums();
Log.d("ArtistRepository", "Got albums directly: " + albums.size());
if (albums.isEmpty()) {
Log.d("ArtistRepository", "No albums found in artist response");
return;
}
randomSongs.setValue(songs);
Collections.shuffle(albums);
int[] counts = albums.stream().mapToInt(AlbumID3::getSongCount).toArray();
Arrays.parallelPrefix(counts, Integer::sum);
int albumLimit = 0;
int multiplier = 4; // get more than the limit so we can shuffle them
while (albumLimit < albums.size() && counts[albumLimit] < count * multiplier)
albumLimit++;
Log.d("ArtistRepository", String.format("Retaining %d/%d albums", albumLimit, albums.size()));
fetchAllAlbumSongsWithCallback(albums.stream().limit(albumLimit).collect(Collectors.toList()), songs -> {
Collections.shuffle(songs);
randomSongs.setValue(songs.stream().limit(count).collect(Collectors.toList()));
});
} else {
Log.d("ArtistRepository", "Failed to get artist info");
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.d("ArtistRepository", "Error getting artist info: " + t.getMessage());
}
});

View File

@@ -18,6 +18,20 @@ public class DownloadRepository {
return downloadDao.getAll();
}
public List<Download> getAllDownloads() {
GetAllDownloadsThreadSafe getDownloads = new GetAllDownloadsThreadSafe(downloadDao);
Thread thread = new Thread(getDownloads);
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
return getDownloads.getDownloads();
}
public Download getDownload(String id) {
Download download = null;
@@ -35,6 +49,24 @@ public class DownloadRepository {
return download;
}
private static class GetAllDownloadsThreadSafe implements Runnable {
private final DownloadDao downloadDao;
private List<Download> downloads;
public GetAllDownloadsThreadSafe(DownloadDao downloadDao) {
this.downloadDao = downloadDao;
}
@Override
public void run() {
downloads = downloadDao.getAllSync();
}
public List<Download> getDownloads() {
return downloads;
}
}
private static class GetDownloadThreadSafe implements Runnable {
private final DownloadDao downloadDao;
private final String id;
@@ -143,6 +175,12 @@ public class DownloadRepository {
thread.start();
}
public void delete(List<String> ids) {
DeleteMultipleThreadSafe delete = new DeleteMultipleThreadSafe(downloadDao, ids);
Thread thread = new Thread(delete);
thread.start();
}
private static class DeleteThreadSafe implements Runnable {
private final DownloadDao downloadDao;
private final String id;
@@ -157,4 +195,19 @@ public class DownloadRepository {
downloadDao.delete(id);
}
}
private static class DeleteMultipleThreadSafe implements Runnable {
private final DownloadDao downloadDao;
private final List<String> ids;
public DeleteMultipleThreadSafe(DownloadDao downloadDao, List<String> ids) {
this.downloadDao = downloadDao;
this.ids = ids;
}
@Override
public void run() {
downloadDao.deleteByIds(ids);
}
}
}

View File

@@ -0,0 +1,92 @@
package com.cappielloantonio.tempo.repository;
import androidx.lifecycle.LiveData;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.LyricsDao;
import com.cappielloantonio.tempo.model.LyricsCache;
public class LyricsRepository {
private final LyricsDao lyricsDao = AppDatabase.getInstance().lyricsDao();
public LyricsCache getLyrics(String songId) {
GetLyricsThreadSafe getLyricsThreadSafe = new GetLyricsThreadSafe(lyricsDao, songId);
Thread thread = new Thread(getLyricsThreadSafe);
thread.start();
try {
thread.join();
return getLyricsThreadSafe.getLyrics();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
public LiveData<LyricsCache> observeLyrics(String songId) {
return lyricsDao.observeOne(songId);
}
public void insert(LyricsCache lyricsCache) {
InsertThreadSafe insert = new InsertThreadSafe(lyricsDao, lyricsCache);
Thread thread = new Thread(insert);
thread.start();
}
public void delete(String songId) {
DeleteThreadSafe delete = new DeleteThreadSafe(lyricsDao, songId);
Thread thread = new Thread(delete);
thread.start();
}
private static class GetLyricsThreadSafe implements Runnable {
private final LyricsDao lyricsDao;
private final String songId;
private LyricsCache lyricsCache;
public GetLyricsThreadSafe(LyricsDao lyricsDao, String songId) {
this.lyricsDao = lyricsDao;
this.songId = songId;
}
@Override
public void run() {
lyricsCache = lyricsDao.getOne(songId);
}
public LyricsCache getLyrics() {
return lyricsCache;
}
}
private static class InsertThreadSafe implements Runnable {
private final LyricsDao lyricsDao;
private final LyricsCache lyricsCache;
public InsertThreadSafe(LyricsDao lyricsDao, LyricsCache lyricsCache) {
this.lyricsDao = lyricsDao;
this.lyricsCache = lyricsCache;
}
@Override
public void run() {
lyricsDao.insert(lyricsCache);
}
}
private static class DeleteThreadSafe implements Runnable {
private final LyricsDao lyricsDao;
private final String songId;
public DeleteThreadSafe(LyricsDao lyricsDao, String songId) {
this.lyricsDao = lyricsDao;
this.songId = songId;
}
@Override
public void run() {
lyricsDao.delete(songId);
}
}
}

View File

@@ -80,21 +80,52 @@ public class PlaylistRepository {
return listLivePlaylistSongs;
}
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
public MutableLiveData<Playlist> getPlaylist(String id) {
MutableLiveData<Playlist> playlistLiveData = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.updatePlaylist(playlistId, null, true, songsId, null)
.getPlaylist(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show();
if (response.isSuccessful()
&& response.body() != null
&& response.body().getSubsonicResponse().getPlaylist() != null) {
playlistLiveData.setValue(response.body().getSubsonicResponse().getPlaylist());
} else {
playlistLiveData.setValue(null);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show();
playlistLiveData.setValue(null);
}
});
return playlistLiveData;
}
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
if (songsId.isEmpty()) {
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
} else{
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.updatePlaylist(playlistId, null, true, songsId, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show();
}
});
}
}
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
@@ -131,23 +162,6 @@ public class PlaylistRepository {
});
}
public void updatePlaylist(String playlistId, String name, boolean isPublic, ArrayList<String> songIdToAdd, ArrayList<Integer> songIndexToRemove) {
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.updatePlaylist(playlistId, name, isPublic, songIdToAdd, songIndexToRemove)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}
public void deletePlaylist(String playlistId) {
App.getSubsonicClientInstance(false)
.getPlaylistClient()

View File

@@ -66,88 +66,33 @@ public class PodcastRepository {
return liveNewestPodcastEpisodes;
}
public void refreshPodcasts() {
App.getSubsonicClientInstance(false)
public Call<ApiResponse> refreshPodcasts() {
return App.getSubsonicClientInstance(false)
.getPodcastClient()
.refreshPodcasts()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
.refreshPodcasts();
}
public void createPodcastChannel(String url) {
App.getSubsonicClientInstance(false)
public Call<ApiResponse> createPodcastChannel(String url) {
return App.getSubsonicClientInstance(false)
.getPodcastClient()
.createPodcastChannel(url)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
.createPodcastChannel(url);
}
public void deletePodcastChannel(String channelId) {
App.getSubsonicClientInstance(false)
public Call<ApiResponse> deletePodcastChannel(String channelId) {
return App.getSubsonicClientInstance(false)
.getPodcastClient()
.deletePodcastChannel(channelId)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
.deletePodcastChannel(channelId);
}
public void deletePodcastEpisode(String episodeId) {
App.getSubsonicClientInstance(false)
public Call<ApiResponse> deletePodcastEpisode(String episodeId) {
return App.getSubsonicClientInstance(false)
.getPodcastClient()
.deletePodcastEpisode(episodeId)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
.deletePodcastEpisode(episodeId);
}
public void downloadPodcastEpisode(String episodeId) {
App.getSubsonicClientInstance(false)
public Call<ApiResponse> downloadPodcastEpisode(String episodeId) {
return App.getSubsonicClientInstance(false)
.getPodcastClient()
.downloadPodcastEpisode(episodeId)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
.downloadPodcastEpisode(episodeId);
}
}

View File

@@ -1,8 +1,11 @@
package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase;
@@ -52,6 +55,8 @@ public class QueueRepository {
public MutableLiveData<PlayQueue> getPlayQueue() {
MutableLiveData<PlayQueue> playQueue = new MutableLiveData<>();
Log.d(TAG, "Getting play queue from server...");
App.getSubsonicClientInstance(false)
.getBookmarksClient()
.getPlayQueue()
@@ -59,12 +64,19 @@ public class QueueRepository {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlayQueue() != null) {
playQueue.setValue(response.body().getSubsonicResponse().getPlayQueue());
PlayQueue serverQueue = response.body().getSubsonicResponse().getPlayQueue();
Log.d(TAG, "Server returned play queue with " +
(serverQueue.getEntries() != null ? serverQueue.getEntries().size() : 0) + " items");
playQueue.setValue(serverQueue);
} else {
Log.d(TAG, "Server returned no play queue");
playQueue.setValue(null);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.e(TAG, "Failed to get play queue", t);
playQueue.setValue(null);
}
});
@@ -73,18 +85,24 @@ public class QueueRepository {
}
public void savePlayQueue(List<String> ids, String current, long position) {
Log.d(TAG, "Saving play queue to server - Items: " + ids.size() + ", Current: " + current);
App.getSubsonicClientInstance(false)
.getBookmarksClient()
.savePlayQueue(ids, current, position)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful()) {
Log.d(TAG, "Play queue saved successfully");
} else {
Log.d(TAG, "Play queue save failed with code: " + response.code());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.e(TAG, "Play queue save failed", t);
}
});
}
@@ -121,6 +139,14 @@ public class QueueRepository {
}
}
private boolean isMediaInQueue(List<Queue> queue, Child media) {
if (queue == null || media == null) return false;
return queue.stream().anyMatch(queueItem ->
queueItem != null && media.getId() != null &&
queueItem.getId().equals(media.getId())
);
}
public void insertAll(List<Child> toAdd, boolean reset, int afterIndex) {
try {
List<Queue> media = new ArrayList<>();
@@ -134,8 +160,14 @@ public class QueueRepository {
media = getMediaThreadSafe.getMedia();
}
for (int i = 0; i < toAdd.size(); i++) {
Queue queueItem = new Queue(toAdd.get(i));
List<Child> filteredToAdd = toAdd;
final List<Queue> finalMedia = media;
filteredToAdd = toAdd.stream()
.filter(child -> !isMediaInQueue(finalMedia, child))
.collect(Collectors.toList());
for (int i = 0; i < filteredToAdd.size(); i++) {
Queue queueItem = new Queue(filteredToAdd.get(i));
media.add(afterIndex + i, queueItem);
}

View File

@@ -38,54 +38,22 @@ public class RadioRepository {
return radioStation;
}
public void createInternetRadioStation(String name, String streamURL, String homepageURL) {
App.getSubsonicClientInstance(false)
public Call<ApiResponse> createInternetRadioStation(String name, String streamURL, String homepageURL) {
return App.getSubsonicClientInstance(false)
.getInternetRadioClient()
.createInternetRadioStation(streamURL, name, homepageURL)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
.createInternetRadioStation(streamURL, name, homepageURL);
}
public void updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) {
App.getSubsonicClientInstance(false)
public Call<ApiResponse> updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) {
return App.getSubsonicClientInstance(false)
.getInternetRadioClient()
.updateInternetRadioStation(id, streamURL, name, homepageURL)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
.updateInternetRadioStation(id, streamURL, name, homepageURL);
}
public void deleteInternetRadioStation(String id) {
App.getSubsonicClientInstance(false)
public Call<ApiResponse> deleteInternetRadioStation(String id) {
return App.getSubsonicClientInstance(false)
.getInternetRadioClient()
.deleteInternetRadioStation(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
.deleteInternetRadioStation(id);
}
}

View File

@@ -13,6 +13,7 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
import com.cappielloantonio.tempo.util.Preferences;
import java.util.ArrayList;
import java.util.LinkedHashSet;
@@ -186,7 +187,12 @@ public class SearchingRepository {
@Override
public void run() {
recent = recentSearchDao.getRecent();
if(Preferences.isSearchSortingChronologicallyEnabled()){
recent = recentSearchDao.getRecent();
}
else {
recent = recentSearchDao.getAlpha();
}
}
public List<String> getRecent() {

View File

@@ -1,22 +1,33 @@
package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse;
import com.cappielloantonio.tempo.util.Constants.SeedType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class SongRepository {
private static final String TAG = "SongRepository";
public interface MediaCallbackInternal {
void onSongsAvailable(List<Child> songs);
}
public MutableLiveData<List<Child>> getStarredSongs(boolean random, int size) {
MutableLiveData<List<Child>> starredSongs = new MutableLiveData<>(Collections.emptyList());
@@ -42,192 +53,332 @@ public class SongRepository {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return starredSongs;
}
public MutableLiveData<List<Child>> getInstantMix(String id, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
/**
* Used by ViewModels. Updates the LiveData list incrementally as songs are found.
*/
public MutableLiveData<List<Child>> getInstantMix(String id, SeedType type, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(new ArrayList<>());
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(id, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
performSmartMix(id, type, count, songs -> {
List<Child> current = instantMix.getValue();
if (current != null) {
for (Child s : songs) {
if (!current.contains(s)) current.add(s);
}
if (current.size() < count / 2) {
fillWithRandom(count - current.size(), remainder -> {
for (Child r : remainder) {
if (!current.contains(r)) current.add(r);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
instantMix.setValue(null);
}
});
instantMix.postValue(current);
});
} else {
instantMix.postValue(current);
}
}
});
return instantMix;
}
public MutableLiveData<List<Child>> getRandomSample(int number, Integer fromYear, Integer toYear) {
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
/**
* Overloaded method used by other Repositories
*/
public void getInstantMix(String id, SeedType type, int count, MediaCallbackInternal callback) {
new MediaCallbackAccumulator(callback, count).start(id, type);
}
private class MediaCallbackAccumulator {
private final MediaCallbackInternal originalCallback;
private final int targetCount;
private final List<Child> accumulatedSongs = new ArrayList<>();
private final Set<String> trackIds = new HashSet<>();
private boolean isComplete = false;
MediaCallbackAccumulator(MediaCallbackInternal callback, int count) {
this.originalCallback = callback;
this.targetCount = count;
}
void start(String id, SeedType type) {
performSmartMix(id, type, targetCount, this::onBatchReceived);
}
private void onBatchReceived(List<Child> batch) {
if (isComplete || batch == null || batch.isEmpty()) {
return;
}
int added = 0;
for (Child song : batch) {
if (!trackIds.contains(song.getId()) && accumulatedSongs.size() < targetCount) {
trackIds.add(song.getId());
accumulatedSongs.add(song);
added++;
}
}
if (accumulatedSongs.size() >= targetCount) {
originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs));
isComplete = true;
}
}
}
private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) {
switch (type) {
case ARTIST:
fetchSimilarByArtist(id, count, callback);
break;
case ALBUM:
fetchAlbumSongsThenSimilar(id, count, callback);
break;
case TRACK:
fetchSingleTrackThenSimilar(id, count, callback);
break;
}
}
private void fetchAlbumSongsThenSimilar(String albumId, int count, MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null &&
response.body().getSubsonicResponse().getAlbum() != null) {
List<Child> albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs();
if (albumSongs != null && !albumSongs.isEmpty()) {
int fromAlbum = Math.min(count, albumSongs.size());
List<Child> limitedAlbumSongs = albumSongs.subList(0, fromAlbum);
callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs));
int remaining = count - fromAlbum;
if (remaining > 0 && albumSongs.get(0).getArtistId() != null) {
fetchSimilarByArtist(albumSongs.get(0).getArtistId(), remaining, callback);
} else if (remaining > 0) {
Log.d(TAG, "No artistId available, skipping similar artist fetch");
}
return;
}
}
Log.d(TAG, "Album fetch failed or empty, calling fillWithRandom");
fillWithRandom(count, callback);
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.d(TAG, "Album fetch failed: " + t.getMessage());
fillWithRandom(count, callback);
}
});
}
private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getRandomSongs(number, fromYear, toYear)
.getBrowsingClient()
.getSimilarSongs2(artistId, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) {
songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs());
List<Child> similar = extractSongs(response, "similarSongs2");
if (!similar.isEmpty()) {
List<Child> limitedSimilar = similar.subList(0, Math.min(count, similar.size()));
callback.onSongsAvailable(limitedSimilar);
} else {
fillWithRandom(count, callback);
}
randomSongsSample.setValue(songs);
}
@Override
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fillWithRandom(count, callback);
}
});
}
private void fetchSingleTrackThenSimilar(String trackId, int count, MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false).getBrowsingClient().getSong(trackId).enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
Child song = response.body().getSubsonicResponse().getSong();
if (song != null) {
callback.onSongsAvailable(Collections.singletonList(song));
int remaining = count - 1;
if (remaining > 0) {
fetchSimilarOnly(trackId, remaining, callback);
}
return;
}
}
fillWithRandom(count, callback);
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fillWithRandom(count, callback);
}
});
}
private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = extractSongs(response, "similarSongs");
if (!songs.isEmpty()) {
List<Child> limitedSongs = songs.subList(0, Math.min(count, songs.size()));
callback.onSongsAvailable(limitedSongs);
} else {
fillWithRandom(count, callback);
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fillWithRandom(count, callback);
}
});
}
private void fillWithRandom(int target, final MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getRandomSongs(target, null, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> random = extractSongs(response, "randomSongs");
if (!random.isEmpty()) {
List<Child> limitedRandom = random.subList(0, Math.min(target, random.size()));
callback.onSongsAvailable(limitedRandom);
} else {
callback.onSongsAvailable(new ArrayList<>());
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
callback.onSongsAvailable(new ArrayList<>());
}
});
}
private List<Child> extractSongs(Response<ApiResponse> response, String type) {
if (response.isSuccessful() && response.body() != null) {
SubsonicResponse res = response.body().getSubsonicResponse();
List<Child> list = null;
if (type.equals("similarSongs") && res.getSimilarSongs() != null) {
list = res.getSimilarSongs().getSongs();
} else if (type.equals("similarSongs2") && res.getSimilarSongs2() != null) {
list = res.getSimilarSongs2().getSongs();
} else if (type.equals("randomSongs") && res.getRandomSongs() != null) {
list = res.getRandomSongs().getSongs();
}
return (list != null) ? list : new ArrayList<>();
}
return new ArrayList<>();
}
public MutableLiveData<List<Child>> getRandomSample(int number, Integer fromYear, Integer toYear) {
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
}
randomSongsSample.setValue(songs);
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return randomSongsSample;
}
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
}
randomSongsSample.setValue(songs);
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return randomSongsSample;
}
public void scrobble(String id, boolean submission) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.scrobble(id, submission)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
}
public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.setRating(id, rating)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getMediaAnnotationClient().setRating(id, rating).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
}
public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) {
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getSongsByGenre(id, 100, 100 * page)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 100, 100 * page).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return songsByGenre;
}
public MutableLiveData<List<Child>> getSongsByGenres(ArrayList<String> genresId) {
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
for (String id : genresId)
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getSongsByGenre(id, 500, 0)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
songs.addAll(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
}
songsByGenre.setValue(songs);
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
for (String id : genresId) {
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 500, 0).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getSongsByGenre().getSongs()));
}
songsByGenre.setValue(songs);
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
}
return songsByGenre;
}
public MutableLiveData<Child> getSong(String id) {
MutableLiveData<Child> song = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSong(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
song.setValue(response.body().getSubsonicResponse().getSong());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getBrowsingClient().getSong(id).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
song.setValue(response.body().getSubsonicResponse().getSong());
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return song;
}
public MutableLiveData<String> getSongLyrics(Child song) {
MutableLiveData<String> lyrics = new MutableLiveData<>(null);
App.getSubsonicClientInstance(false)
.getMediaRetrievalClient()
.getLyrics(song.getArtist(), song.getTitle())
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getMediaRetrievalClient().getLyrics(song.getArtist(), song.getTitle()).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return lyrics;
}
}
}

View File

@@ -0,0 +1,590 @@
package com.cappielloantonio.tempo.service
import android.annotation.SuppressLint
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.repository.QueueRepository
import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.*
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@UnstableApi
open class BaseMediaService : MediaLibraryService() {
companion object {
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF =
"android.media3.session.demo.REPEAT_OFF"
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE =
"android.media3.session.demo.REPEAT_ONE"
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
"android.media3.session.demo.REPEAT_ALL"
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
}
protected lateinit var exoplayer: ExoPlayer
protected lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var networkCallback: CustomNetworkCallback
private lateinit var equalizerManager: EqualizerManager
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
private var widgetUpdateScheduled = false
private val widgetUpdateRunnable = object : Runnable {
override fun run() {
val player = mediaLibrarySession.player
if (!player.isPlaying) {
widgetUpdateScheduled = false
return
}
updateWidget(player)
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
}
}
private val binder = LocalBinder()
open fun playerInitHook() {
initializeExoPlayer()
initializeMediaLibrarySession(exoplayer)
initializePlayerListener(exoplayer)
setPlayer(null, exoplayer)
}
open fun getMediaLibrarySessionCallback(): MediaLibrarySession.Callback {
return CustomMediaLibrarySessionCallback(baseContext)
}
fun updateMediaItems(player: Player) {
Log.d(javaClass.toString(), "update items")
val n = player.mediaItemCount
val k = player.currentMediaItemIndex
val current = player.currentPosition
val items = (0..n - 1).map { MappingUtil.mapMediaItem(player.getMediaItemAt(it)) }
player.clearMediaItems()
player.setMediaItems(items, k, current)
}
fun restorePlayerFromQueue(player: Player) {
if (player.mediaItemCount > 0) return
val queueRepository = QueueRepository()
val storedQueue = queueRepository.media
if (storedQueue.isNullOrEmpty()) return
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
if (mediaItems.isEmpty()) return
val lastIndex = try {
queueRepository.lastPlayedMediaIndex
} catch (_: Exception) {
0
}.coerceIn(0, mediaItems.size - 1)
val lastPosition = try {
queueRepository.lastPlayedMediaTimestamp
} catch (_: Exception) {
0L
}.let { if (it < 0L) 0L else it }
player.setMediaItems(mediaItems, lastIndex, lastPosition)
player.prepare()
updateWidget(player)
}
fun initializePlayerListener(player: Player) {
player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
Log.d(javaClass.toString(), "onMediaItemTransition" + player.currentMediaItemIndex)
if (mediaItem == null) return
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
MediaManager.setLastPlayedTimestamp(mediaItem)
}
updateWidget(player)
}
override fun onTracksChanged(tracks: Tracks) {
Log.d(javaClass.toString(), "onTracksChanged " + player.currentMediaItemIndex)
ReplayGainUtil.setReplayGain(player, tracks)
val currentMediaItem = player.currentMediaItem
if (currentMediaItem != null) {
val item = MappingUtil.mapMediaItem(currentMediaItem)
if (item.mediaMetadata.extras != null)
MediaManager.scrobble(item, false)
if (player.nextMediaItemIndex == C.INDEX_UNSET)
MediaManager.continuousPlay(player.currentMediaItem)
}
if (player is ExoPlayer) {
// https://stackoverflow.com/questions/56937283/exoplayer-shuffle-doesnt-reproduce-all-the-songs
if (MediaManager.justStarted.get()) {
Log.d(javaClass.toString(), "update shuffle order")
MediaManager.justStarted.set(false)
val shuffledList = IntArray(player.mediaItemCount) { i -> i }
shuffledList.shuffle()
val index = shuffledList.indexOf(player.currentMediaItemIndex)
// swap current media index to the first index
if (index > -1 && shuffledList.isNotEmpty()) {
val tmp = shuffledList[0]
shuffledList[0] = shuffledList[index]
shuffledList[index] = tmp
}
player.shuffleOrder =
DefaultShuffleOrder(shuffledList, kotlin.random.Random.nextLong())
}
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
Log.d(javaClass.toString(), "onIsPlayingChanged " + player.currentMediaItemIndex)
if (!isPlaying) {
MediaManager.setPlayingPausedTimestamp(
player.currentMediaItem,
player.currentPosition
)
} else {
MediaManager.scrobble(player.currentMediaItem, false)
}
if (isPlaying) {
scheduleWidgetUpdates()
} else {
stopWidgetUpdates()
}
updateWidget(player)
}
override fun onPlaybackStateChanged(playbackState: Int) {
Log.d(javaClass.toString(), "onPlaybackStateChanged")
super.onPlaybackStateChanged(playbackState)
if (!player.hasNextMediaItem() &&
playbackState == Player.STATE_ENDED &&
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
) {
MediaManager.scrobble(player.currentMediaItem, true)
MediaManager.saveChronology(player.currentMediaItem)
}
updateWidget(player)
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
Log.d(javaClass.toString(), "onPositionDiscontinuity")
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
MediaManager.scrobble(oldPosition.mediaItem, true)
MediaManager.saveChronology(oldPosition.mediaItem)
}
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
}
}
}
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
}
override fun onRepeatModeChanged(repeatMode: Int) {
Preferences.setRepeatMode(repeatMode)
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
Log.d(javaClass.toString(), "onAudioSessionIdChanged")
attachEqualizerIfPossible(audioSessionId)
}
})
if (player.isPlaying) {
scheduleWidgetUpdates()
}
}
fun setPlayer(oldPlayer: Player?, newPlayer: Player) {
if (oldPlayer === newPlayer) return
if (oldPlayer != null) {
val currentQueue = getQueueFromPlayer(oldPlayer)
val currentIndex = oldPlayer.currentMediaItemIndex
val currentPosition = oldPlayer.currentPosition
val isPlaying = oldPlayer.playWhenReady
oldPlayer.stop()
newPlayer.setMediaItems(currentQueue, currentIndex, currentPosition)
newPlayer.playWhenReady = isPlaying
newPlayer.prepare()
}
mediaLibrarySession.player = newPlayer
}
open fun releasePlayers() {
exoplayer.release()
}
fun getQueueFromPlayer(player: Player): List<MediaItem> {
return (0..player.mediaItemCount - 1).map(player::getMediaItemAt)
}
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaLibrarySession.player
if (!player.playWhenReady || player.mediaItemCount == 0) {
stopSelf()
}
}
override fun onCreate() {
super.onCreate()
playerInitHook()
initializeEqualizerManager()
initializeNetworkListener()
restorePlayerFromQueue(mediaLibrarySession.player)
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onDestroy() {
releaseNetworkCallback()
equalizerManager.release()
stopWidgetUpdates()
releasePlayers()
mediaLibrarySession.release()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
// Check if the intent is for our custom equalizer binder
if (intent?.action == ACTION_BIND_EQUALIZER) {
return binder
}
// Otherwise, handle it as a normal MediaLibraryService connection
return super.onBind(intent)
}
private fun initializeExoPlayer() {
exoplayer = ExoPlayer.Builder(this)
.setRenderersFactory(getRenderersFactory())
.setMediaSourceFactory(getMediaSourceFactory())
.setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK)
.setLoadControl(initializeLoadControl())
.build()
exoplayer.shuffleModeEnabled = Preferences.isShuffleModeEnabled()
exoplayer.repeatMode = Preferences.getRepeatMode()
}
private fun initializeEqualizerManager() {
equalizerManager = EqualizerManager()
val audioSessionId = exoplayer.audioSessionId
attachEqualizerIfPossible(audioSessionId)
}
private fun initializeMediaLibrarySession(player: Player) {
Log.d(javaClass.toString(), "initializeMediaLibrarySession")
val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run {
addNextIntent(Intent(baseContext, MainActivity::class.java))
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
}
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, getMediaLibrarySessionCallback())
.setSessionActivity(sessionActivityPendingIntent)
.build()
}
private fun initializeNetworkListener() {
networkCallback = CustomNetworkCallback()
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(
networkCallback
)
updateMediaItems(mediaLibrarySession.player)
}
private fun initializeLoadControl(): DefaultLoadControl {
return DefaultLoadControl.Builder()
.setBufferDurationsMs(
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
)
.build()
}
private fun releaseNetworkCallback() {
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
}
private fun updateWidget(player: Player) {
val mi = player.currentMediaItem
val title = mi?.mediaMetadata?.title?.toString()
?: mi?.mediaMetadata?.extras?.getString("title")
val artist = mi?.mediaMetadata?.artist?.toString()
?: mi?.mediaMetadata?.extras?.getString("artist")
val album = mi?.mediaMetadata?.albumTitle?.toString()
?: mi?.mediaMetadata?.extras?.getString("album")
val extras = mi?.mediaMetadata?.extras
val coverId = extras?.getString("coverArtId")
val songLink = extras?.getString("assetLinkSong")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
val albumLink = extras?.getString("assetLinkAlbum")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
val artistLink = extras?.getString("assetLinkArtist")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
WidgetUpdateManager.updateFromState(
this,
title ?: "",
artist ?: "",
album ?: "",
coverId,
player.isPlaying,
player.shuffleModeEnabled,
player.repeatMode,
position,
duration,
songLink,
albumLink,
artistLink
)
}
private fun scheduleWidgetUpdates() {
if (widgetUpdateScheduled) return
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
widgetUpdateScheduled = true
}
private fun stopWidgetUpdates() {
if (!widgetUpdateScheduled) return
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
widgetUpdateScheduled = false
}
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
if (audioSessionId == 0 || audioSessionId == -1) return false
val attached = equalizerManager.attachToSession(audioSessionId)
if (attached) {
val enabled = Preferences.isEqualizerEnabled()
equalizerManager.setEnabled(enabled)
val bands = equalizerManager.getNumberOfBands()
val savedLevels = Preferences.getEqualizerBandLevels(bands)
for (i in 0 until bands) {
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
}
sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED))
}
return attached
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this)
@UnstableApi
private class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
private val shuffleCommands: List<CommandButton>
private val repeatCommands: List<CommandButton>
constructor(ctx: Context) {
shuffleCommands = listOf(
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON,
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF
)
.map { getShuffleCommandButton(SessionCommand(it, Bundle.EMPTY), ctx) }
repeatCommands = listOf(
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE,
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL
)
.map { getRepeatCommandButton(SessionCommand(it, Bundle.EMPTY), ctx) }
}
override fun onConnect(
session: MediaSession,
controller: ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
(shuffleCommands + repeatCommands).forEach { commandButton ->
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
val result = MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(availableSessionCommands.build())
.setAvailablePlayerCommands(connectionResult.availablePlayerCommands)
.setMediaButtonPreferences(buildCustomLayout(session.player))
.build()
return result
}
override fun onCustomCommand(
session: MediaSession,
controller: ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
Log.d(javaClass.toString(), "onCustomCommand")
when (customCommand.customAction) {
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL,
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> {
val nextMode = when (session.player.repeatMode) {
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE
else -> Player.REPEAT_MODE_OFF
}
session.player.repeatMode = nextMode
}
}
session.setMediaButtonPreferences(buildCustomLayout(session.player))
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
Log.d(javaClass.toString(), "onAddMediaItems")
val updatedMediaItems = mediaItems.map { mediaItem ->
val mediaMetadata = mediaItem.mediaMetadata
val newMetadata = mediaMetadata.buildUpon()
.setArtist(
if (mediaMetadata.artist != null) mediaMetadata.artist
else mediaMetadata.extras?.getString("uri") ?: ""
)
.build()
mediaItem.buildUpon()
.setUri(mediaItem.requestMetadata.mediaUri)
.setMediaMetadata(newMetadata)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.build()
}
return Futures.immediateFuture(updatedMediaItems)
}
@SuppressLint("PrivateResource")
private fun getShuffleCommandButton(
sessionCommand: SessionCommand,
ctx: Context
): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder(if (isOn) CommandButton.ICON_SHUFFLE_OFF else CommandButton.ICON_SHUFFLE_ON)
.setSessionCommand(sessionCommand)
.setDisplayName(
ctx.getString(
if (isOn) R.string.exo_controls_shuffle_on_description
else R.string.exo_controls_shuffle_off_description
)
)
.build()
}
@SuppressLint("PrivateResource")
private fun getRepeatCommandButton(
sessionCommand: SessionCommand,
ctx: Context
): CommandButton {
val icon = when (sessionCommand.customAction) {
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> CommandButton.ICON_REPEAT_ONE
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> CommandButton.ICON_REPEAT_ALL
else -> CommandButton.ICON_REPEAT_OFF
}
val description = when (sessionCommand.customAction) {
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.string.exo_controls_repeat_one_description
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.string.exo_controls_repeat_all_description
else -> R.string.exo_controls_repeat_off_description
}
return CommandButton.Builder(icon)
.setSessionCommand(sessionCommand)
.setDisplayName(ctx.getString(description))
.build()
}
private fun buildCustomLayout(player: Player): ImmutableList<CommandButton> {
val shuffle = shuffleCommands[if (player.shuffleModeEnabled) 1 else 0]
val repeat = when (player.repeatMode) {
Player.REPEAT_MODE_ONE -> repeatCommands[1]
Player.REPEAT_MODE_ALL -> repeatCommands[2]
else -> repeatCommands[0]
}
return ImmutableList.of(shuffle, repeat)
}
}
private inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
var wasWifi = false
init {
val manager = getSystemService(ConnectivityManager::class.java)
val network = manager.activeNetwork
val capabilities = manager.getNetworkCapabilities(network)
if (capabilities != null)
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
if (isWifi != wasWifi) {
wasWifi = isWifi
widgetUpdateHandler.post {
updateMediaItems(mediaLibrarySession.player)
}
}
}
}
inner class LocalBinder : Binder() {
fun getEqualizerManager(): EqualizerManager {
return equalizerManager
}
}
}
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L

View File

@@ -0,0 +1,47 @@
package com.cappielloantonio.tempo.service
import android.media.audiofx.Equalizer
class EqualizerManager {
private var equalizer: Equalizer? = null
fun attachToSession(audioSessionId: Int): Boolean {
release()
if (audioSessionId != 0 && audioSessionId != -1) {
try {
equalizer = Equalizer(0, audioSessionId).apply {
enabled = true
}
return true
} catch (e: Exception) {
// Some devices may not support Equalizer or audio session may be invalid
equalizer = null
}
}
return false
}
fun setBandLevel(band: Short, level: Short) {
equalizer?.setBandLevel(band, level)
}
fun getNumberOfBands(): Short = equalizer?.numberOfBands ?: 0
fun getBandLevelRange(): ShortArray? = equalizer?.bandLevelRange
fun getCenterFreq(band: Short): Int? =
equalizer?.getCenterFreq(band)?.div(1000)
fun getBandLevel(band: Short): Short? =
equalizer?.getBandLevel(band)
fun setEnabled(enabled: Boolean) {
equalizer?.enabled = enabled
}
fun release() {
equalizer?.release()
equalizer = null
}
}

View File

@@ -1,11 +1,18 @@
package com.cappielloantonio.tempo.service;
import android.content.ComponentName;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
@@ -19,16 +26,88 @@ import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
import com.cappielloantonio.tempo.util.Constants.SeedType;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class MediaManager {
private static final String TAG = "MediaManager";
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
public static AtomicBoolean justStarted = new AtomicBoolean(false);
private static final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
public static void registerPlaybackObserver(
ListenableFuture<MediaBrowser> browserFuture,
PlaybackViewModel playbackViewModel
) {
if (browserFuture == null) return;
Futures.addCallback(browserFuture, new FutureCallback<MediaBrowser>() {
@Override
public void onSuccess(MediaBrowser browser) {
MediaBrowser current = attachedBrowserRef.get();
if (current != browser) {
browser.addListener(new Player.Listener() {
@Override
public void onEvents(@NonNull Player player, @NonNull Player.Events events) {
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)
|| events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
|| events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
String mediaId = player.getCurrentMediaItem() != null
? player.getCurrentMediaItem().mediaId
: null;
boolean playing = player.getPlaybackState() == Player.STATE_READY
&& player.getPlayWhenReady();
playbackViewModel.update(mediaId, playing);
}
}
});
String mediaId = browser.getCurrentMediaItem() != null
? browser.getCurrentMediaItem().mediaId
: null;
boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady();
playbackViewModel.update(mediaId, playing);
attachedBrowserRef = new WeakReference<>(browser);
} else {
String mediaId = browser.getCurrentMediaItem() != null
? browser.getCurrentMediaItem().mediaId
: null;
boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady();
playbackViewModel.update(mediaId, playing);
}
}
@Override
public void onFailure(@NonNull Throwable t) {
Log.e(TAG, "Failed to get MediaBrowser instance", t);
}
}, MoreExecutors.directExecutor());
}
public static void onBrowserReleased(@Nullable MediaBrowser released) {
MediaBrowser attached = attachedBrowserRef.get();
if (attached == released) {
attachedBrowserRef.clear();
}
}
public static void reset(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
if (mediaBrowserListenableFuture != null) {
@@ -102,20 +181,46 @@ public class MediaManager {
}
}
@OptIn(markerClass = UnstableApi.class)
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems();
mediaBrowserListenableFuture.get().setMediaItems(MappingUtil.mapMediaItems(media));
mediaBrowserListenableFuture.get().prepare();
mediaBrowserListenableFuture.get().seekTo(startIndex, 0);
mediaBrowserListenableFuture.get().play();
enqueueDatabase(media, true, 0);
final MediaBrowser browser = mediaBrowserListenableFuture.get();
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
new Handler(Looper.getMainLooper()).post(() -> {
justStarted.set(true);
browser.setMediaItems(items, startIndex, 0);
browser.prepare();
Player.Listener timelineListener = new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
int itemCount = browser.getMediaItemCount();
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
browser.seekTo(startIndex, 0);
browser.play();
browser.removeListener(this);
} else {
Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex);
}
}
};
browser.addListener(timelineListener);
});
backgroundExecutor.execute(() -> {
Log.d(TAG, "Background: enqueuing to database");
enqueueDatabase(media, true, 0);
});
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
Log.e(TAG, "Error in startQueue: " + e.getMessage(), e);
}
}, MoreExecutors.directExecutor());
}
@@ -126,10 +231,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems();
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(media));
mediaBrowserListenableFuture.get().prepare();
mediaBrowserListenableFuture.get().play();
MediaBrowser browser = mediaBrowserListenableFuture.get();
justStarted.set(true);
browser.setMediaItem(MappingUtil.mapMediaItem(media));
browser.prepare();
browser.play();
enqueueDatabase(media, true, 0);
}
} catch (ExecutionException | InterruptedException e) {
@@ -139,15 +245,35 @@ public class MediaManager {
}
}
public static void playDownloadedMediaItem(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, MediaItem mediaItem) {
if (mediaBrowserListenableFuture != null && mediaItem != null) {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
justStarted.set(true);
mediaBrowser.setMediaItem(mediaItem);
mediaBrowser.prepare();
mediaBrowser.play();
clearDatabase();
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, MoreExecutors.directExecutor());
}
}
public static void startRadio(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems();
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation));
mediaBrowserListenableFuture.get().prepare();
mediaBrowserListenableFuture.get().play();
MediaBrowser browser = mediaBrowserListenableFuture.get();
justStarted.set(true);
browser.setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation));
browser.prepare();
browser.play();
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
@@ -161,10 +287,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems();
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(podcastEpisode));
mediaBrowserListenableFuture.get().prepare();
mediaBrowserListenableFuture.get().play();
MediaBrowser browser = mediaBrowserListenableFuture.get();
justStarted.set(true);
browser.setMediaItem(MappingUtil.mapMediaItem(podcastEpisode));
browser.prepare();
browser.play();
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
@@ -178,9 +305,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) {
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex());
mediaBrowserListenableFuture.get().addMediaItems(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItems(media));
Log.e(TAG, "enqueue");
MediaBrowser browser = mediaBrowserListenableFuture.get();
if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) {
enqueueDatabase(media, false, browser.getNextMediaItemIndex());
browser.addMediaItems(browser.getNextMediaItemIndex(), MappingUtil.mapMediaItems(media));
} else {
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount());
mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media));
@@ -198,9 +327,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) {
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex());
mediaBrowserListenableFuture.get().addMediaItem(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItem(media));
Log.e(TAG, "enqueue");
MediaBrowser browser = mediaBrowserListenableFuture.get();
if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) {
enqueueDatabase(media, false, browser.getNextMediaItemIndex());
browser.addMediaItem(browser.getNextMediaItemIndex(), MappingUtil.mapMediaItem(media));
} else {
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount());
mediaBrowserListenableFuture.get().addMediaItem(MappingUtil.mapMediaItem(media));
@@ -218,8 +349,10 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().removeMediaItems(startIndex, endIndex + 1);
mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1));
Log.e(TAG, "shuffle");
MediaBrowser browser = mediaBrowserListenableFuture.get();
browser.removeMediaItems(startIndex, endIndex + 1);
browser.addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1));
swapDatabase(media);
}
} catch (ExecutionException | InterruptedException e) {
@@ -234,6 +367,7 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
Log.e(TAG, "swap");
mediaBrowserListenableFuture.get().moveMediaItem(from, to);
swapDatabase(media);
}
@@ -249,6 +383,7 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
Log.e(TAG, "remove");
if (mediaBrowserListenableFuture.get().getMediaItemCount() > 1 && mediaBrowserListenableFuture.get().getCurrentMediaItemIndex() != toRemove) {
mediaBrowserListenableFuture.get().removeMediaItem(toRemove);
removeDatabase(media, toRemove);
@@ -268,6 +403,7 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
Log.e(TAG, "remove range");
mediaBrowserListenableFuture.get().removeMediaItems(fromItem, toItem);
removeRangeDatabase(media, fromItem, toItem);
}
@@ -312,11 +448,12 @@ public class MediaManager {
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
Preferences.setLastInstantMix();
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10);
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, SeedType.TRACK, 10);
instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> media) {
if (media != null) {
Log.e(TAG, "continuous play");
ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder(
App.getContext(),
new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class))

View File

@@ -2,22 +2,28 @@ package com.cappielloantonio.tempo.subsonic
import com.cappielloantonio.tempo.App
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
import com.google.gson.GsonBuilder
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.Date
import java.util.concurrent.TimeUnit
class RetrofitClient(subsonic: Subsonic) {
var retrofit: Retrofit
init {
val gson = GsonBuilder()
.registerTypeAdapter(Date::class.java, EmptyDateTypeAdapter())
.setLenient()
.create()
retrofit = Retrofit.Builder()
.baseUrl(subsonic.url)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create()))
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
.addConverterFactory(GsonConverterFactory.create(gson))
.client(getOkHttpClient())
.build()
}

View File

@@ -7,7 +7,7 @@ import java.util.UUID;
public class SubsonicPreferences {
private String serverUrl;
private String username;
private String clientName = "Tempo";
private String clientName = "Tempus";
private SubsonicAuthentication authentication;
public String getServerUrl() {

View File

@@ -34,6 +34,11 @@ public class AlbumSongListClient {
return albumSongListService.getRandomSongs(subsonic.getParams(), size, fromYear, toYear);
}
public Call<ApiResponse> getRandomSongs(int size, Integer fromYear, Integer toYear, String genre) {
Log.d(TAG, "getRandomSongs()");
return albumSongListService.getRandomSongs(subsonic.getParams(), size, fromYear, toYear, genre);
}
public Call<ApiResponse> getSongsByGenre(String genre, int count, int offset) {
Log.d(TAG, "getSongsByGenre()");
return albumSongListService.getSongsByGenre(subsonic.getParams(), genre, count, offset);

View File

@@ -19,6 +19,9 @@ public interface AlbumSongListService {
@GET("getRandomSongs")
Call<ApiResponse> getRandomSongs(@QueryMap Map<String, String> params, @Query("size") int size, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear);
@GET("getRandomSongs")
Call<ApiResponse> getRandomSongs(@QueryMap Map<String, String> params, @Query("size") int size, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear, @Query("genre") String genre);
@GET("getSongsByGenre")
Call<ApiResponse> getSongsByGenre(@QueryMap Map<String, String> params, @Query("genre") String genre, @Query("count") int count, @Query("offset") int offset);

View File

@@ -5,6 +5,9 @@ import android.util.Log;
import com.cappielloantonio.tempo.subsonic.RetrofitClient;
import com.cappielloantonio.tempo.subsonic.Subsonic;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.util.Preferences;
import java.util.concurrent.TimeUnit;
import retrofit2.Call;
@@ -21,7 +24,15 @@ public class SystemClient {
public Call<ApiResponse> ping() {
Log.d(TAG, "ping()");
return systemService.ping(subsonic.getParams());
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
if (Preferences.isInUseServerAddressLocal()) {
pingCall.timeout()
.timeout(1, TimeUnit.SECONDS);
} else {
pingCall.timeout()
.timeout(3, TimeUnit.SECONDS);
}
return pingCall;
}
public Call<ApiResponse> getLicense() {

View File

@@ -4,38 +4,36 @@ import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
import java.time.Instant
import java.time.LocalDate
import java.util.*
import java.util.Date
@Keep
@Parcelize
open class AlbumID3 : Parcelable {
var id: String? = null
var name: String? = null
var artist: String? = null
var artistId: String? = null
open class AlbumID3(
var id: String? = null,
var name: String? = null,
var artist: String? = null,
var artistId: String? = null,
@SerializedName("coverArt")
var coverArtId: String? = null
var songCount: Int? = 0
var duration: Int? = 0
var playCount: Long? = 0
var created: Date? = null
var starred: Date? = null
var year: Int = 0
var genre: String? = null
var played: Date? = Date(0)
var userRating: Int? = 0
var recordLabels: List<RecordLabel>? = null
var musicBrainzId: String? = null
var genres: List<ItemGenre>? = null
var artists: List<ArtistID3>? = null
var displayArtist: String? = null
var releaseTypes: List<String>? = null
var moods: List<String>? = null
var sortName: String? = null
var originalReleaseDate: ItemDate? = null
var releaseDate: ItemDate? = null
var isCompilation: Boolean? = null
var discTitles: List<DiscTitle>? = null
}
var coverArtId: String? = null,
var songCount: Int? = 0,
var duration: Int? = 0,
var playCount: Long? = 0,
var created: Date? = null,
var starred: Date? = null,
var year: Int = 0,
var genre: String? = null,
var played: Date? = Date(0),
var userRating: Int? = 0,
var recordLabels: List<RecordLabel>? = null,
var musicBrainzId: String? = null,
var genres: List<ItemGenre>? = null,
var artists: List<ArtistID3>? = null,
var displayArtist: String? = null,
var releaseTypes: List<String>? = null,
var moods: List<String>? = null,
var sortName: String? = null,
var originalReleaseDate: ItemDate? = null,
var releaseDate: ItemDate? = null,
var isCompilation: Boolean? = null,
var discTitles: List<DiscTitle>? = null,
) : Parcelable

View File

@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
class AlbumWithSongsID3 : AlbumID3(), Parcelable {
class AlbumWithSongsID3(
@SerializedName("song")
var songs: List<Child>? = null
}
var songs: List<Child>? = null,
) : AlbumID3(), Parcelable

View File

@@ -7,10 +7,10 @@ import java.util.Date
@Keep
@Parcelize
class Artist : Parcelable {
var id: String? = null
var name: String? = null
var starred: Date? = null
var userRating: Int? = null
var averageRating: Double? = null
}
class Artist(
var id: String? = null,
var name: String? = null,
var starred: Date? = null,
var userRating: Int? = null,
var averageRating: Double? = null,
) : Parcelable

View File

@@ -4,15 +4,15 @@ import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Keep
@Parcelize
open class ArtistID3 : Parcelable {
var id: String? = null
var name: String? = null
open class ArtistID3(
var id: String? = null,
var name: String? = null,
@SerializedName("coverArt")
var coverArtId: String? = null
var albumCount = 0
var starred: Date? = null
}
var coverArtId: String? = null,
var albumCount: Int = 0,
var starred: Date? = null,
) : Parcelable

View File

@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
class ArtistWithAlbumsID3 : ArtistID3(), Parcelable {
class ArtistWithAlbumsID3(
@SerializedName("album")
var albums: List<AlbumID3>? = null
}
var albums: List<AlbumID3>? = null,
) : ArtistID3(), Parcelable

View File

@@ -50,6 +50,12 @@ open class Child(
@ColumnInfo("bitrate")
@SerializedName("bitRate")
var bitrate: Int? = null,
@ColumnInfo("sampling_rate")
@SerializedName("samplingRate")
var samplingRate: Int? = null,
@ColumnInfo("bit_depth")
@SerializedName("bitDepth")
var bitDepth: Int? = null,
@ColumnInfo
var path: String? = null,
@ColumnInfo(name = "is_video")

View File

@@ -8,15 +8,15 @@ import java.util.Date
@Keep
@Parcelize
class Directory : Parcelable {
class Directory(
@SerializedName("child")
var children: List<Child>? = null
var id: String? = null
var children: List<Child>? = null,
var id: String? = null,
@SerializedName("parent")
var parentId: String? = null
var name: String? = null
var starred: Date? = null
var userRating: Int? = null
var averageRating: Double? = null
var playCount: Long? = null
}
var parentId: String? = null,
var name: String? = null,
var starred: Date? = null,
var userRating: Int? = null,
var averageRating: Double? = null,
var playCount: Long? = null,
) : Parcelable

View File

@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
open class DiscTitle : Parcelable {
var disc: Int? = null
var title: String? = null
}
open class DiscTitle(
var disc: Int? = null,
var title: String? = null,
) : Parcelable

View File

@@ -7,9 +7,9 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
class Genre : Parcelable {
class Genre(
@SerializedName("value")
var genre: String? = null
var songCount = 0
var albumCount = 0
}
var genre: String? = null,
var songCount: Int = 0,
var albumCount: Int = 0,
) : Parcelable

View File

@@ -6,9 +6,9 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
class InternetRadioStation : Parcelable {
var id: String? = null
var name: String? = null
var streamUrl: String? = null
var homePageUrl: String? = null
}
class InternetRadioStation(
var id: String? = null,
var name: String? = null,
var streamUrl: String? = null,
var homePageUrl: String? = null,
) : Parcelable

View File

@@ -9,16 +9,24 @@ import java.util.Locale
@Keep
@Parcelize
open class ItemDate : Parcelable {
var year: Int? = null
var month: Int? = null
var day: Int? = null
open class ItemDate(
var year: Int? = null,
var month: Int? = null,
var day: Int? = null,
) : Parcelable {
fun getFormattedDate(): String? {
if (year == null && month == null && day == null) return null
fun getFormattedDate(): String {
val calendar = Calendar.getInstance()
val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault())
val dateFormat = if (month == null && day == null) {
SimpleDateFormat("yyyy", Locale.getDefault())
} else if (day == null) {
SimpleDateFormat("MMMM yyyy", Locale.getDefault())
} else {
SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault())
}
calendar.set(year ?: 0, month ?: 0, day ?: 0)
calendar.set(year ?: 0, month ?: 1, day ?: 1)
return dateFormat.format(calendar.time)
}

View File

@@ -6,6 +6,6 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
open class ItemGenre : Parcelable {
var name: String? = null
}
open class ItemGenre(
var name: String? = null,
) : Parcelable

View File

@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
class MusicFolder : Parcelable {
var id: String? = null
var name: String? = null
}
class MusicFolder(
var id: String? = null,
var name: String? = null,
) : Parcelable

View File

@@ -8,10 +8,9 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class NowPlayingEntry(
@SerializedName("_id")
override val id: String
) : Child(id) {
var username: String? = null
var minutesAgo = 0
var playerId = 0
var playerName: String? = null
}
override val id: String,
var username: String? = null,
var minutesAgo: Int = 0,
var playerId: Int = 0,
var playerName: String? = null,
) : Child(id)

View File

@@ -7,8 +7,9 @@ import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Keep
@Parcelize
@@ -16,27 +17,57 @@ import java.util.*
open class Playlist(
@PrimaryKey
@ColumnInfo(name = "id")
open var id: String
) : Parcelable {
open var id: String,
@ColumnInfo(name = "name")
var name: String? = null
var name: String? = null,
@ColumnInfo(name = "duration")
var duration: Long = 0,
@SerializedName("coverArt")
@ColumnInfo(name = "coverArt")
var coverArtId: String? = null,
) : Parcelable {
@Ignore
@IgnoredOnParcel
var comment: String? = null
@Ignore
@IgnoredOnParcel
var owner: String? = null
@Ignore
@IgnoredOnParcel
@SerializedName("public")
var isUniversal: Boolean? = null
@Ignore
@IgnoredOnParcel
var songCount: Int = 0
@ColumnInfo(name = "duration")
var duration: Long = 0
@Ignore
@IgnoredOnParcel
var created: Date? = null
@Ignore
@IgnoredOnParcel
var changed: Date? = null
@ColumnInfo(name = "coverArt")
var coverArtId: String? = null
@Ignore
@IgnoredOnParcel
var allowedUsers: List<String>? = null
@Ignore
constructor(
id: String,
name: String?,
comment: String?,
owner: String?,
isUniversal: Boolean?,
songCount: Int,
duration: Long,
created: Date?,
changed: Date?,
coverArtId: String?,
allowedUsers: List<String>?,
) : this(id, name, duration, coverArtId) {
this.comment = comment
this.owner = owner
this.isUniversal = isUniversal
this.songCount = songCount
this.created = created
this.changed = changed
this.allowedUsers = allowedUsers
}
}

View File

@@ -9,8 +9,7 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class PlaylistWithSongs(
@SerializedName("_id")
override var id: String
) : Playlist(id), Parcelable {
override var id: String,
@SerializedName("entry")
var entries: List<Child>? = null
}
var entries: List<Child>? = null,
) : Playlist(id), Parcelable

View File

@@ -6,5 +6,5 @@ import com.google.gson.annotations.SerializedName
@Keep
class Playlists(
@SerializedName("playlist")
var playlists: List<Playlist>? = null
var playlists: List<Playlist>? = null,
)

View File

@@ -3,20 +3,21 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
class PodcastChannel : Parcelable {
class PodcastChannel(
@SerializedName("episode")
var episodes: List<PodcastEpisode>? = null
var id: String? = null
var url: String? = null
var title: String? = null
var description: String? = null
var episodes: List<PodcastEpisode>? = null,
var id: String? = null,
var url: String? = null,
var title: String? = null,
var description: String? = null,
@SerializedName("coverArt")
var coverArtId: String? = null
var originalImageUrl: String? = null
var status: String? = null
var errorMessage: String? = null
}
var coverArtId: String? = null,
var originalImageUrl: String? = null,
var status: String? = null,
var errorMessage: String? = null,
) : Parcelable

View File

@@ -3,37 +3,38 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.*
@Keep
@Parcelize
class PodcastEpisode : Parcelable {
var id: String? = null
class PodcastEpisode(
var id: String? = null,
@SerializedName("parent")
var parentId: String? = null
var isDir = false
var title: String? = null
var album: String? = null
var artist: String? = null
var year: Int? = null
var genre: String? = null
var parentId: String? = null,
var isDir: Boolean = false,
var title: String? = null,
var album: String? = null,
var artist: String? = null,
var year: Int? = null,
var genre: String? = null,
@SerializedName("coverArt")
var coverArtId: String? = null
var size: Long? = null
var contentType: String? = null
var suffix: String? = null
var duration: Int? = null
var coverArtId: String? = null,
var size: Long? = null,
var contentType: String? = null,
var suffix: String? = null,
var duration: Int? = null,
@SerializedName("bitRate")
var bitrate: Int? = null
var path: String? = null
var isVideo: Boolean = false
var created: Date? = null
var artistId: String? = null
var type: String? = null
var streamId: String? = null
var channelId: String? = null
var description: String? = null
var status: String? = null
var publishDate: Date? = null
}
var bitrate: Int? = null,
var path: String? = null,
var isVideo: Boolean = false,
var created: Date? = null,
var artistId: String? = null,
var type: String? = null,
var streamId: String? = null,
var channelId: String? = null,
var description: String? = null,
var status: String? = null,
var publishDate: Date? = null,
) : Parcelable

View File

@@ -2,10 +2,11 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
open class RecordLabel : Parcelable {
var name: String? = null
}
open class RecordLabel(
var name: String? = null,
) : Parcelable

View File

@@ -3,20 +3,21 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Keep
@Parcelize
class Share : Parcelable {
data class Share(
@SerializedName("entry")
var entries: List<Child>? = null
var id: String? = null
var url: String? = null
var description: String? = null
var username: String? = null
var created: Date? = null
var expires: Date? = null
var lastVisited: Date? = null
var visitCount = 0
}
var entries: List<Child>? = null,
var id: String? = null,
var url: String? = null,
var description: String? = null,
var username: String? = null,
var created: Date? = null,
var expires: Date? = null,
var lastVisited: Date? = null,
var visitCount: Int = 0
) : Parcelable

View File

@@ -1,8 +1,10 @@
package com.cappielloantonio.tempo.subsonic.models
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
class SimilarSongs {
@SerializedName("song")
var songs: List<Child>? = null
}

View File

@@ -38,21 +38,36 @@ public class CacheUtil {
return chain.proceed(request);
};
private boolean isConnected() {
ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
Network network = connectivityManager.getActiveNetwork();
if (network != null) {
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
if (capabilities != null) {
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
}
}
if (connectivityManager == null) {
return false;
}
return false;
Network network = connectivityManager.getActiveNetwork();
if (network == null) {
return false;
}
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
if (capabilities == null) {
return false;
}
boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
if (!hasInternet) {
return false;
}
boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
if (!hasAppropriateTransport) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,42 @@
package com.cappielloantonio.tempo.subsonic.utils
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import java.lang.reflect.Type
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
// This adapter handles Date objects, returning null if the JSON string is empty or unparsable.
class EmptyDateTypeAdapter : JsonDeserializer<Date> {
// Define the date formats expected from the Subsonic server.
private val dateFormats: List<SimpleDateFormat> = listOf(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") },
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") },
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }
)
@Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Date? {
val jsonString = json.asString.trim()
if (jsonString.isEmpty()) {
return null
}
for (format in dateFormats) {
try {
return format.parse(jsonString)
} catch (e: ParseException) {
// Ignore and try the next format
}
}
return null
}
}

View File

@@ -1,11 +1,14 @@
package com.cappielloantonio.tempo.ui.activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
@@ -13,7 +16,10 @@ import androidx.annotation.NonNull;
import androidx.core.splashscreen.SplashScreen;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
@@ -31,6 +37,8 @@ import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
import com.cappielloantonio.tempo.util.AssetLinkNavigator;
import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
@@ -54,8 +62,11 @@ public class MainActivity extends BaseActivity {
private BottomNavigationView bottomNavigationView;
public NavController navController;
private BottomSheetBehavior bottomSheetBehavior;
private AssetLinkNavigator assetLinkNavigator;
private AssetLinkUtil.AssetLink pendingAssetLink;
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
private Intent pendingDownloadPlaybackIntent;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -69,6 +80,7 @@ public class MainActivity extends BaseActivity {
setContentView(view);
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
assetLinkNavigator = new AssetLinkNavigator(this);
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
connectivityStatusReceiverManager(true);
@@ -77,12 +89,16 @@ public class MainActivity extends BaseActivity {
checkConnectionType();
getOpenSubsonicExtensions();
checkTempoUpdate();
maybeSchedulePlaybackIntent(getIntent());
}
@Override
protected void onStart() {
super.onStart();
pingServer();
initService();
consumePendingPlaybackIntent();
}
@Override
@@ -98,6 +114,14 @@ public class MainActivity extends BaseActivity {
bind = null;
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
maybeSchedulePlaybackIntent(intent);
consumePendingPlaybackIntent();
}
@Override
public void onBackPressed() {
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
@@ -292,6 +316,24 @@ public class MainActivity extends BaseActivity {
public void goFromLogin() {
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
goToHome();
consumePendingAssetLink();
}
public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink) {
openAssetLink(assetLink, true);
}
public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink, boolean collapsePlayer) {
if (!isUserAuthenticated()) {
pendingAssetLink = assetLink;
return;
}
if (collapsePlayer) {
setBottomSheetInPeek(true);
}
if (assetLinkNavigator != null) {
assetLinkNavigator.open(assetLink);
}
}
public void quit() {
@@ -316,6 +358,7 @@ public class MainActivity extends BaseActivity {
Preferences.setSkipSilenceMode(false);
Preferences.setDataSavingMode(false);
Preferences.setStarredSyncEnabled(false);
Preferences.setStarredAlbumsSyncEnabled(false);
}
private void resetMusicSession() {
@@ -350,6 +393,7 @@ public class MainActivity extends BaseActivity {
Preferences.switchInUseServerAddress();
App.refreshSubsonicClient();
pingServer();
resetView();
} else {
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
}
@@ -360,6 +404,7 @@ public class MainActivity extends BaseActivity {
Preferences.switchInUseServerAddress();
App.refreshSubsonicClient();
pingServer();
resetView();
} else {
mainViewModel.ping().observe(this, subsonicResponse -> {
if (subsonicResponse == null) {
@@ -375,6 +420,13 @@ public class MainActivity extends BaseActivity {
}
}
private void resetView() {
resetViewModel();
int id = Objects.requireNonNull(navController.getCurrentDestination()).getId();
navController.popBackStack(id, true);
navController.navigate(id);
}
private void getOpenSubsonicExtensions() {
if (Preferences.getToken() != null) {
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
@@ -386,7 +438,7 @@ public class MainActivity extends BaseActivity {
}
private void checkTempoUpdate() {
if (BuildConfig.FLAVOR.equals("tempo") && Preferences.showTempoUpdateDialog()) {
if (BuildConfig.FLAVOR.equals("tempus") && Preferences.isGithubUpdateEnabled() && Preferences.showTempusUpdateDialog()) {
mainViewModel.checkTempoUpdate().observe(this, latestRelease -> {
if (latestRelease != null && UpdateUtil.showUpdateDialog(latestRelease)) {
GithubTempoUpdateDialog dialog = new GithubTempoUpdateDialog(latestRelease);
@@ -407,4 +459,98 @@ public class MainActivity extends BaseActivity {
}
}
}
}
private void maybeSchedulePlaybackIntent(Intent intent) {
if (intent == null) return;
if (Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD.equals(intent.getAction())
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
pendingDownloadPlaybackIntent = new Intent(intent);
}
handleAssetLinkIntent(intent);
}
private void consumePendingPlaybackIntent() {
if (pendingDownloadPlaybackIntent == null) return;
Intent intent = pendingDownloadPlaybackIntent;
pendingDownloadPlaybackIntent = null;
playDownloadedMedia(intent);
}
private void handleAssetLinkIntent(Intent intent) {
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.parse(intent);
if (assetLink == null) {
return;
}
if (!isUserAuthenticated()) {
pendingAssetLink = assetLink;
intent.setData(null);
return;
}
if (assetLinkNavigator != null) {
assetLinkNavigator.open(assetLink);
}
intent.setData(null);
}
private boolean isUserAuthenticated() {
return Preferences.getPassword() != null
|| (Preferences.getToken() != null && Preferences.getSalt() != null);
}
private void consumePendingAssetLink() {
if (pendingAssetLink == null || assetLinkNavigator == null) {
return;
}
assetLinkNavigator.open(pendingAssetLink);
pendingAssetLink = null;
}
private void playDownloadedMedia(Intent intent) {
String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI);
if (TextUtils.isEmpty(uriString)) {
return;
}
Uri uri = Uri.parse(uriString);
String mediaId = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID);
if (TextUtils.isEmpty(mediaId)) {
mediaId = uri.toString();
}
String title = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_TITLE);
String artist = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ARTIST);
String album = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ALBUM);
int duration = intent.getIntExtra(Constants.EXTRA_DOWNLOAD_DURATION, 0);
Bundle extras = new Bundle();
extras.putString("id", mediaId);
extras.putString("title", title);
extras.putString("artist", artist);
extras.putString("album", album);
extras.putString("uri", uri.toString());
extras.putString("type", Constants.MEDIA_TYPE_MUSIC);
extras.putInt("duration", duration);
MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder()
.setExtras(extras)
.setIsBrowsable(false)
.setIsPlayable(true);
if (!TextUtils.isEmpty(title)) metadataBuilder.setTitle(title);
if (!TextUtils.isEmpty(artist)) metadataBuilder.setArtist(artist);
if (!TextUtils.isEmpty(album)) metadataBuilder.setAlbumTitle(album);
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(mediaId)
.setMediaMetadata(metadataBuilder.build())
.setUri(uri)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setRequestMetadata(new MediaItem.RequestMetadata.Builder()
.setMediaUri(uri)
.setExtras(extras)
.build())
.build();
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
}
}

View File

@@ -20,6 +20,7 @@ import com.cappielloantonio.tempo.util.MusicUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAdapter.ViewHolder> implements Filterable {
@@ -151,13 +152,27 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
}
}
public void setItemsWithoutFilter(List<AlbumID3> albums) {
this.albumsFull = new ArrayList<>(albums);
this.albums = new ArrayList<>(albums);
notifyDataSetChanged();
}
public void sort(String order) {
if (albums == null) return;
switch (order) {
case Constants.ALBUM_ORDER_BY_NAME:
albums.sort(Comparator.comparing(AlbumID3::getName));
albums.sort(Comparator.comparing(
album -> album.getName() != null ? album.getName() : "",
String.CASE_INSENSITIVE_ORDER
));
break;
case Constants.ALBUM_ORDER_BY_ARTIST:
albums.sort(Comparator.comparing(AlbumID3::getArtist, Comparator.nullsLast(Comparator.naturalOrder())));
albums.sort(Comparator.comparing(
album -> album.getArtist() != null ? album.getArtist() : "",
String.CASE_INSENSITIVE_ORDER
));
break;
case Constants.ALBUM_ORDER_BY_YEAR:
albums.sort(Comparator.comparing(AlbumID3::getYear));
@@ -166,15 +181,23 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
Collections.shuffle(albums);
break;
case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED:
albums.sort(Comparator.comparing(AlbumID3::getCreated));
albums.sort(Comparator.comparing(
album -> album.getCreated() != null ? album.getCreated() : new Date(0),
Comparator.nullsLast(Date::compareTo)
));
Collections.reverse(albums);
break;
case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
albums.sort(Comparator.comparing(AlbumID3::getPlayed));
albums.sort(Comparator.comparing(
album -> album.getPlayed() != null ? album.getPlayed() : new Date(0),
Comparator.nullsLast(Date::compareTo)
));
Collections.reverse(albums);
break;
case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
albums.sort(Comparator.comparing(AlbumID3::getPlayCount));
albums.sort(Comparator.comparing(
album -> album.getPlayCount() != null ? album.getPlayCount() : 0L
));
Collections.reverse(albums);
break;
}

View File

@@ -151,6 +151,9 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
case Constants.ARTIST_ORDER_BY_RANDOM:
Collections.shuffle(artists);
break;
case Constants.ARTIST_ORDER_BY_ALBUM_COUNT:
artists.sort(Comparator.comparing(ArtistID3::getAlbumCount).reversed());
break;
}
notifyDataSetChanged();

View File

@@ -73,6 +73,11 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
this.item = item;
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> {
onLongClick();
return true;
});
}
public void onClick() {
@@ -82,6 +87,13 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
click.onMediaClick(bundle);
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
click.onMediaLongClick(bundle);
return true;
}
}
private void startAnimation(ViewHolder holder) {

View File

@@ -191,7 +191,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
R.string.song_subtitle_formatter,
song.getArtist(),
MusicUtil.getReadableDurationString(song.getDuration(), false),
""
MusicUtil.getReadableAudioQualityString(song)
)
);

View File

@@ -53,7 +53,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
.into(holder.item.musicDirectoryCoverImageView);
holder.item.musicDirectoryMoreButton.setVisibility(child.isDir() ? View.VISIBLE : View.INVISIBLE);
holder.item.musicDirectoryPlayButton.setVisibility(child.isDir() ? View.INVISIBLE : View.VISIBLE);
holder.item.musicDirectoryPlayButton.setVisibility(child.isDir() ? View.VISIBLE : View.INVISIBLE);
}
@Override
@@ -80,6 +80,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
itemView.setOnLongClickListener(v -> onLongClick());
item.musicDirectoryMoreButton.setOnClickListener(v -> onClick());
item.musicDirectoryPlayButton.setOnClickListener(v -> onPlayClick());
}
public void onClick() {
@@ -107,5 +108,13 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
return false;
}
}
public void onPlayClick() {
if (children.get(getBindingAdapterPosition()).isDir()) {
Bundle bundle = new Bundle();
bundle.putString(Constants.MUSIC_DIRECTORY_ID, children.get(getBindingAdapterPosition()).getId());
click.onMusicDirectoryPlay(bundle);
}
}
}
}

View File

@@ -76,6 +76,7 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
itemView.setOnClickListener(v -> onClick());
item.musicIndexMoreButton.setOnClickListener(v -> onClick());
item.musicIndexPlayButton.setOnClickListener(v -> onPlayClick());
}
public void onClick() {
@@ -83,5 +84,11 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId());
click.onMusicIndexClick(bundle);
}
public void onPlayClick() {
Bundle bundle = new Bundle();
bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId());
click.onMusicIndexPlay(bundle);
}
}
}

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -17,22 +18,34 @@ import com.cappielloantonio.tempo.databinding.ItemPlayerQueueSongBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
import com.cappielloantonio.tempo.service.DownloaderManager;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> {
private static final String TAG = "PlayerSongQueueAdapter";
private final ClickCallback click;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private List<Child> songs;
private final Map<String, Boolean> downloadStatusCache = new ConcurrentHashMap<>();
private String currentPlayingId;
private boolean isPlaying;
private List<Integer> currentPlayingPositions = Collections.emptyList();
public PlayerSongQueueAdapter(ClickCallback click) {
this.click = click;
@@ -70,7 +83,6 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
.build()
.thumbnail(thumbnail)
.into(holder.item.queueSongCoverImageView);
MediaManager.getCurrentIndex(mediaBrowserListenableFuture, new MediaIndexCallback() {
@Override
public void onRecovery(int index) {
@@ -86,6 +98,23 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
}
});
boolean isDownloaded = false;
if (Preferences.getDownloadDirectoryUri() == null) {
DownloaderManager downloaderManager = DownloadUtil.getDownloadTracker(holder.itemView.getContext());
if (downloaderManager != null) {
isDownloaded = downloaderManager.isDownloaded(song.getId());
}
} else {
isDownloaded = ExternalAudioReader.getUri(song) != null;
}
if (isDownloaded) {
holder.item.downloadIndicatorIcon.setVisibility(View.VISIBLE);
} else {
holder.item.downloadIndicatorIcon.setVisibility(View.GONE);
}
if (Preferences.showItemRating()) {
if (song.getStarred() == null && song.getUserRating() == null) {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
@@ -104,8 +133,48 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
} else {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
}
holder.itemView.setOnClickListener(v -> {
mediaBrowserListenableFuture.addListener(() -> {
try {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
int pos = holder.getBindingAdapterPosition();
Child s = songs.get(pos);
if (currentPlayingId != null && currentPlayingId.equals(s.getId())) {
if (isPlaying) {
mediaBrowser.pause();
} else {
mediaBrowser.play();
}
} else {
mediaBrowser.seekTo(pos, 0);
mediaBrowser.play();
}
} catch (Exception e) {
Log.w(TAG, "Error obtaining MediaBrowser", e);
}
}, MoreExecutors.directExecutor());
});
bindPlaybackState(holder, song);
}
private void bindPlaybackState(@NonNull PlayerSongQueueAdapter.ViewHolder holder, @NonNull Child song) {
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
if (isCurrent) {
holder.item.playPauseIcon.setVisibility(View.VISIBLE);
if (isPlaying) {
holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause);
} else {
holder.item.playPauseIcon.setImageResource(R.drawable.ic_play);
}
holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
} else {
holder.item.playPauseIcon.setVisibility(View.INVISIBLE);
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
}
}
public List<Child> getItems() {
return this.songs;
}
@@ -132,6 +201,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
}
public void setPlaybackState(String mediaId, boolean playing) {
String oldId = this.currentPlayingId;
boolean oldPlaying = this.isPlaying;
List<Integer> oldPositions = currentPlayingPositions;
this.currentPlayingId = mediaId;
this.isPlaying = playing;
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
List<Integer> newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
if (oldPositions.equals(newPositionsCheck)) {
return;
}
}
currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
for (int pos : oldPositions) {
if (pos >= 0 && pos < songs.size()) {
notifyItemChanged(pos, "payload_playback");
}
}
for (int pos : currentPlayingPositions) {
if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) {
notifyItemChanged(pos, "payload_playback");
}
}
}
private List<Integer> findPositionsById(String id) {
if (id == null) return Collections.emptyList();
List<Integer> positions = new ArrayList<>();
for (int i = 0; i < songs.size(); i++) {
if (id.equals(songs.get(i).getId())) {
positions.add(i);
}
}
return positions;
}
public Child getItem(int id) {
return songs.get(id);
}

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