tunzo-player 1.0.33 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,7 +48,7 @@ class TunzoPlayerAPI {
48
48
  return __awaiter(this, arguments, void 0, function* (id, limit = 100) {
49
49
  var _a;
50
50
  try {
51
- const response = yield fetch(`${this.baseUrl}/songs/${id}/suggestions?limit=${limit}`);
51
+ const response = yield fetch(`https://tunzo-api.vercel.app/api/songs/${id}/suggestions?limit=${limit}`);
52
52
  if (!response.ok) {
53
53
  throw new Error(`HTTP error! status: ${response.status}`);
54
54
  }
@@ -14,8 +14,10 @@ export declare class Player {
14
14
  private static selectedQuality;
15
15
  private static intendedPlaying;
16
16
  private static toastCtrl;
17
+ private static isAndroid;
17
18
  /** Initialize with playlist and quality */
18
19
  static initialize(playlist: any[], quality?: number): void;
20
+ private static setupAndroidPlayer;
19
21
  static setToastController(controller: ToastController): void;
20
22
  /** Setup audio element for better compatibility */
21
23
  private static setupAudioElement;
@@ -23,6 +25,7 @@ export declare class Player {
23
25
  /** Call this once on user gesture to unlock audio in WebView */
24
26
  static unlockAudio(): void;
25
27
  static play(song: any, index?: number): void;
28
+ private static playAndroid;
26
29
  static pause(): void;
27
30
  static resume(): void;
28
31
  static togglePlayPause(): void;
@@ -12,15 +12,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.Player = void 0;
13
13
  const rxjs_1 = require("rxjs");
14
14
  const keep_awake_1 = require("@capacitor-community/keep-awake");
15
+ const core_1 = require("@capacitor/core");
16
+ const capacitor_plugin_playlist_1 = require("capacitor-plugin-playlist");
15
17
  class Player {
16
18
  /** Initialize with playlist and quality */
17
19
  static initialize(playlist, quality = 3) {
18
20
  this.playlist = playlist;
19
21
  this.selectedQuality = quality;
20
- this.setupMediaSession();
21
- this.setupAudioElement();
22
+ if (this.isAndroid) {
23
+ this.setupAndroidPlayer();
24
+ }
25
+ else {
26
+ this.setupMediaSession();
27
+ this.setupAudioElement();
28
+ }
22
29
  this.startWatchdog();
23
30
  }
31
+ static setupAndroidPlayer() {
32
+ capacitor_plugin_playlist_1.Playlist.addListener('status', (data) => {
33
+ if (data && data.status && (data.status.msgType === capacitor_plugin_playlist_1.RmxAudioStatusMessage.RMXSTATUS_COMPLETED ||
34
+ data.status.msgType === capacitor_plugin_playlist_1.RmxAudioStatusMessage.RMXSTATUS_PLAYLIST_COMPLETED)) {
35
+ this.autoNext();
36
+ }
37
+ });
38
+ }
24
39
  static setToastController(controller) {
25
40
  this.toastCtrl = controller;
26
41
  }
@@ -67,17 +82,24 @@ class Player {
67
82
  }
68
83
  static startWatchdog() {
69
84
  setInterval(() => {
70
- if (this.intendedPlaying && this.audio.paused && this.currentSong) {
85
+ if (this.intendedPlaying && (this.isAndroid ? false : this.audio.paused) && this.currentSong) {
71
86
  console.log('Watchdog: Audio paused unexpectedly. Attempting to resume...');
72
- this.audio.play().catch(e => console.warn('Watchdog resume failed:', e));
87
+ if (this.isAndroid) {
88
+ capacitor_plugin_playlist_1.Playlist.play().catch(e => console.warn('Watchdog resume failed:', e));
89
+ }
90
+ else {
91
+ this.audio.play().catch(e => console.warn('Watchdog resume failed:', e));
92
+ }
73
93
  }
74
94
  }, 10000);
75
95
  }
76
96
  /** Call this once on user gesture to unlock audio in WebView */
77
97
  static unlockAudio() {
78
- this.audio.src = '';
79
- this.audio.load();
80
- this.audio.play().catch(() => { });
98
+ if (!this.isAndroid) {
99
+ this.audio.src = '';
100
+ this.audio.load();
101
+ this.audio.play().catch(() => { });
102
+ }
81
103
  }
82
104
  static play(song, index = 0) {
83
105
  var _a;
@@ -91,35 +113,93 @@ class Player {
91
113
  if (url.startsWith('http://')) {
92
114
  url = url.replace('http://', 'https://');
93
115
  }
94
- this.audio.src = url;
95
- this.audio.load();
96
- this.audio.play().then(() => {
97
- this.isPlaying = true;
98
- this.updateMediaSessionMetadata(song);
99
- keep_awake_1.KeepAwake.keepAwake(); // Keep screen/CPU awake
100
- if ('mediaSession' in navigator) {
101
- navigator.mediaSession.playbackState = 'playing';
116
+ if (this.isAndroid) {
117
+ this.playAndroid(song, url);
118
+ }
119
+ else {
120
+ this.audio.src = url;
121
+ this.audio.load();
122
+ this.audio.play().then(() => {
123
+ this.isPlaying = true;
124
+ this.updateMediaSessionMetadata(song);
125
+ keep_awake_1.KeepAwake.keepAwake(); // Keep screen/CPU awake
126
+ if ('mediaSession' in navigator) {
127
+ navigator.mediaSession.playbackState = 'playing';
128
+ }
129
+ }).catch((err) => {
130
+ this.isPlaying = false;
131
+ console.warn('Audio play failed:', err);
132
+ });
133
+ }
134
+ }
135
+ static playAndroid(song, url) {
136
+ return __awaiter(this, void 0, void 0, function* () {
137
+ var _a, _b;
138
+ try {
139
+ // Extract artwork
140
+ let artworkUrl = '';
141
+ if (song.image && Array.isArray(song.image)) {
142
+ const highQualityImage = song.image[song.image.length - 1];
143
+ if (highQualityImage && highQualityImage.url) {
144
+ artworkUrl = highQualityImage.url;
145
+ }
146
+ }
147
+ // Extract artist
148
+ let artistName = 'Unknown Artist';
149
+ if (((_a = song.artists) === null || _a === void 0 ? void 0 : _a.primary) && Array.isArray(song.artists.primary) && song.artists.primary.length > 0) {
150
+ artistName = song.artists.primary.map((artist) => artist.name).join(', ');
151
+ }
152
+ else if (song.primaryArtists) {
153
+ artistName = song.primaryArtists;
154
+ }
155
+ else if (song.artist) {
156
+ artistName = song.artist;
157
+ }
158
+ const track = {
159
+ trackId: song.id,
160
+ assetUrl: url,
161
+ albumArt: artworkUrl,
162
+ artist: artistName,
163
+ album: ((_b = song.album) === null || _b === void 0 ? void 0 : _b.name) || 'Unknown Album',
164
+ title: song.name || song.title || 'Unknown Title'
165
+ };
166
+ yield capacitor_plugin_playlist_1.Playlist.clearAllItems();
167
+ yield capacitor_plugin_playlist_1.Playlist.addItem({ item: track });
168
+ yield capacitor_plugin_playlist_1.Playlist.play();
169
+ this.isPlaying = true;
170
+ keep_awake_1.KeepAwake.keepAwake();
171
+ }
172
+ catch (e) {
173
+ console.error('Android play failed:', e);
174
+ this.isPlaying = false;
102
175
  }
103
- }).catch((err) => {
104
- this.isPlaying = false;
105
- console.warn('Audio play failed:', err);
106
176
  });
107
177
  }
108
178
  static pause() {
109
179
  this.intendedPlaying = false;
110
- this.audio.pause();
111
180
  this.isPlaying = false;
181
+ if (this.isAndroid) {
182
+ capacitor_plugin_playlist_1.Playlist.pause();
183
+ }
184
+ else {
185
+ this.audio.pause();
186
+ }
112
187
  keep_awake_1.KeepAwake.allowSleep();
113
- if ('mediaSession' in navigator) {
188
+ if (!this.isAndroid && 'mediaSession' in navigator) {
114
189
  navigator.mediaSession.playbackState = 'paused';
115
190
  }
116
191
  }
117
192
  static resume() {
118
193
  this.intendedPlaying = true;
119
- this.audio.play();
120
194
  this.isPlaying = true;
195
+ if (this.isAndroid) {
196
+ capacitor_plugin_playlist_1.Playlist.play();
197
+ }
198
+ else {
199
+ this.audio.play();
200
+ }
121
201
  keep_awake_1.KeepAwake.keepAwake();
122
- if ('mediaSession' in navigator) {
202
+ if (!this.isAndroid && 'mediaSession' in navigator) {
123
203
  navigator.mediaSession.playbackState = 'playing';
124
204
  }
125
205
  }
@@ -154,8 +234,13 @@ class Player {
154
234
  }
155
235
  }
156
236
  static seek(seconds) {
157
- this.audio.currentTime = seconds;
158
- this.updatePositionState();
237
+ if (this.isAndroid) {
238
+ capacitor_plugin_playlist_1.Playlist.seekTo({ position: seconds });
239
+ }
240
+ else {
241
+ this.audio.currentTime = seconds;
242
+ this.updatePositionState();
243
+ }
159
244
  }
160
245
  static autoNext() {
161
246
  this.next();
@@ -310,3 +395,4 @@ Player.queue$ = new rxjs_1.BehaviorSubject([]);
310
395
  Player.playlist = [];
311
396
  Player.selectedQuality = 3;
312
397
  Player.intendedPlaying = false;
398
+ Player.isAndroid = core_1.Capacitor.getPlatform() === 'android';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunzo-player",
3
- "version": "1.0.33",
3
+ "version": "1.0.39",
4
4
  "description": "A music playback service for Angular and Ionic apps with native audio control support.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,7 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@capacitor-community/keep-awake": "^7.1.0",
33
+ "@capacitor/core": "^8.0.0",
33
34
  "@ionic/angular": "^8.7.11",
35
+ "capacitor-plugin-playlist": "^0.7.4",
34
36
  "rxjs": "^7.8.2"
35
37
  }
36
38
  }
@@ -36,7 +36,7 @@ export class TunzoPlayerAPI {
36
36
  async suggesstedSongs(id: string, limit: number = 100): Promise<any[]> {
37
37
  try {
38
38
  const response = await fetch(
39
- `${this.baseUrl}/songs/${id}/suggestions?limit=${limit}`
39
+ `https://tunzo-api.vercel.app/api/songs/${id}/suggestions?limit=${limit}`
40
40
  );
41
41
 
42
42
  if (!response.ok) {
@@ -1,6 +1,8 @@
1
1
  import { BehaviorSubject } from 'rxjs';
2
2
  import { KeepAwake } from '@capacitor-community/keep-awake';
3
3
  import { ToastController } from '@ionic/angular/standalone';
4
+ import { Capacitor } from '@capacitor/core';
5
+ import { Playlist, AudioTrack, RmxAudioStatusMessage } from 'capacitor-plugin-playlist';
4
6
 
5
7
  export class Player {
6
8
  private static audio = new Audio();
@@ -16,16 +18,32 @@ export class Player {
16
18
  private static selectedQuality = 3;
17
19
  private static intendedPlaying = false;
18
20
  private static toastCtrl: ToastController;
21
+ private static isAndroid = Capacitor.getPlatform() === 'android';
19
22
 
20
23
  /** Initialize with playlist and quality */
21
24
  static initialize(playlist: any[], quality = 3) {
22
25
  this.playlist = playlist;
23
26
  this.selectedQuality = quality;
24
- this.setupMediaSession();
25
- this.setupAudioElement();
27
+ if (this.isAndroid) {
28
+ this.setupAndroidPlayer();
29
+ } else {
30
+ this.setupMediaSession();
31
+ this.setupAudioElement();
32
+ }
26
33
  this.startWatchdog();
27
34
  }
28
35
 
36
+ private static setupAndroidPlayer() {
37
+ Playlist.addListener('status', (data) => {
38
+ if (data && data.status && (
39
+ data.status.msgType === RmxAudioStatusMessage.RMXSTATUS_COMPLETED ||
40
+ data.status.msgType === RmxAudioStatusMessage.RMXSTATUS_PLAYLIST_COMPLETED
41
+ )) {
42
+ this.autoNext();
43
+ }
44
+ });
45
+ }
46
+
29
47
  static setToastController(controller: ToastController) {
30
48
  this.toastCtrl = controller;
31
49
  }
@@ -81,18 +99,24 @@ export class Player {
81
99
 
82
100
  private static startWatchdog() {
83
101
  setInterval(() => {
84
- if (this.intendedPlaying && this.audio.paused && this.currentSong) {
102
+ if (this.intendedPlaying && (this.isAndroid ? false : this.audio.paused) && this.currentSong) {
85
103
  console.log('Watchdog: Audio paused unexpectedly. Attempting to resume...');
86
- this.audio.play().catch(e => console.warn('Watchdog resume failed:', e));
104
+ if (this.isAndroid) {
105
+ Playlist.play().catch(e => console.warn('Watchdog resume failed:', e));
106
+ } else {
107
+ this.audio.play().catch(e => console.warn('Watchdog resume failed:', e));
108
+ }
87
109
  }
88
110
  }, 10000);
89
111
  }
90
112
 
91
113
  /** Call this once on user gesture to unlock audio in WebView */
92
114
  static unlockAudio() {
93
- this.audio.src = '';
94
- this.audio.load();
95
- this.audio.play().catch(() => { });
115
+ if (!this.isAndroid) {
116
+ this.audio.src = '';
117
+ this.audio.load();
118
+ this.audio.play().catch(() => { });
119
+ }
96
120
  }
97
121
 
98
122
  static play(song: any, index: number = 0) {
@@ -110,38 +134,95 @@ export class Player {
110
134
  url = url.replace('http://', 'https://');
111
135
  }
112
136
 
113
- this.audio.src = url;
114
- this.audio.load();
137
+ if (this.isAndroid) {
138
+ this.playAndroid(song, url);
139
+ } else {
140
+ this.audio.src = url;
141
+ this.audio.load();
142
+
143
+ this.audio.play().then(() => {
144
+ this.isPlaying = true;
145
+ this.updateMediaSessionMetadata(song);
146
+ KeepAwake.keepAwake(); // Keep screen/CPU awake
147
+ if ('mediaSession' in navigator) {
148
+ navigator.mediaSession.playbackState = 'playing';
149
+ }
150
+ }).catch((err) => {
151
+ this.isPlaying = false;
152
+ console.warn('Audio play failed:', err);
153
+ });
154
+ }
155
+ }
115
156
 
116
- this.audio.play().then(() => {
117
- this.isPlaying = true;
118
- this.updateMediaSessionMetadata(song);
119
- KeepAwake.keepAwake(); // Keep screen/CPU awake
120
- if ('mediaSession' in navigator) {
121
- navigator.mediaSession.playbackState = 'playing';
157
+ private static async playAndroid(song: any, url: string) {
158
+ try {
159
+ // Extract artwork
160
+ let artworkUrl = '';
161
+ if (song.image && Array.isArray(song.image)) {
162
+ const highQualityImage = song.image[song.image.length - 1];
163
+ if (highQualityImage && highQualityImage.url) {
164
+ artworkUrl = highQualityImage.url;
165
+ }
122
166
  }
123
- }).catch((err) => {
167
+
168
+ // Extract artist
169
+ let artistName = 'Unknown Artist';
170
+ if (song.artists?.primary && Array.isArray(song.artists.primary) && song.artists.primary.length > 0) {
171
+ artistName = song.artists.primary.map((artist: any) => artist.name).join(', ');
172
+ } else if (song.primaryArtists) {
173
+ artistName = song.primaryArtists;
174
+ } else if (song.artist) {
175
+ artistName = song.artist;
176
+ }
177
+
178
+ const track: AudioTrack = {
179
+ trackId: song.id,
180
+ assetUrl: url,
181
+ albumArt: artworkUrl,
182
+ artist: artistName,
183
+ album: song.album?.name || 'Unknown Album',
184
+ title: song.name || song.title || 'Unknown Title'
185
+ };
186
+
187
+ await Playlist.clearAllItems();
188
+ await Playlist.addItem({ item: track });
189
+ await Playlist.play();
190
+ this.isPlaying = true;
191
+ KeepAwake.keepAwake();
192
+ } catch (e) {
193
+ console.error('Android play failed:', e);
124
194
  this.isPlaying = false;
125
- console.warn('Audio play failed:', err);
126
- });
195
+ }
127
196
  }
128
197
 
129
198
  static pause() {
130
199
  this.intendedPlaying = false;
131
- this.audio.pause();
132
200
  this.isPlaying = false;
201
+
202
+ if (this.isAndroid) {
203
+ Playlist.pause();
204
+ } else {
205
+ this.audio.pause();
206
+ }
207
+
133
208
  KeepAwake.allowSleep();
134
- if ('mediaSession' in navigator) {
209
+ if (!this.isAndroid && 'mediaSession' in navigator) {
135
210
  navigator.mediaSession.playbackState = 'paused';
136
211
  }
137
212
  }
138
213
 
139
214
  static resume() {
140
215
  this.intendedPlaying = true;
141
- this.audio.play();
142
216
  this.isPlaying = true;
217
+
218
+ if (this.isAndroid) {
219
+ Playlist.play();
220
+ } else {
221
+ this.audio.play();
222
+ }
223
+
143
224
  KeepAwake.keepAwake();
144
- if ('mediaSession' in navigator) {
225
+ if (!this.isAndroid && 'mediaSession' in navigator) {
145
226
  navigator.mediaSession.playbackState = 'playing';
146
227
  }
147
228
  }
@@ -176,8 +257,12 @@ export class Player {
176
257
  }
177
258
 
178
259
  static seek(seconds: number) {
179
- this.audio.currentTime = seconds;
180
- this.updatePositionState();
260
+ if (this.isAndroid) {
261
+ Playlist.seekTo({ position: seconds });
262
+ } else {
263
+ this.audio.currentTime = seconds;
264
+ this.updatePositionState();
265
+ }
181
266
  }
182
267
 
183
268
  static autoNext() {