tunzo-player 1.0.0
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.
- package/dist/core/api-calles.d.ts +9 -0
- package/dist/core/api-calles.js +38 -0
- package/dist/core/player.d.ts +35 -0
- package/dist/core/player.js +133 -0
- package/dist/core/settings.d.ts +33 -0
- package/dist/core/settings.js +61 -0
- package/dist/core/theme.d.ts +25 -0
- package/dist/core/theme.js +57 -0
- package/dist/core/tunzo-player.d.ts +0 -0
- package/dist/core/tunzo-player.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/package.json +16 -0
- package/src/core/api-calles.ts +26 -0
- package/src/core/player.ts +152 -0
- package/src/core/settings.ts +69 -0
- package/src/core/theme.ts +61 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class TunzoPlayerAPI {
|
|
2
|
+
/**
|
|
3
|
+
* Search for songs using the saavn.dev API
|
|
4
|
+
* @param query Search keyword (e.g., artist name, song name)
|
|
5
|
+
* @param limit Number of results to return (default: 250)
|
|
6
|
+
* @returns Array of song result objects
|
|
7
|
+
*/
|
|
8
|
+
searchSongs(query: string, limit?: number): Promise<any[]>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.TunzoPlayerAPI = void 0;
|
|
13
|
+
class TunzoPlayerAPI {
|
|
14
|
+
/**
|
|
15
|
+
* Search for songs using the saavn.dev API
|
|
16
|
+
* @param query Search keyword (e.g., artist name, song name)
|
|
17
|
+
* @param limit Number of results to return (default: 250)
|
|
18
|
+
* @returns Array of song result objects
|
|
19
|
+
*/
|
|
20
|
+
searchSongs(query_1) {
|
|
21
|
+
return __awaiter(this, arguments, void 0, function* (query, limit = 250) {
|
|
22
|
+
var _a;
|
|
23
|
+
try {
|
|
24
|
+
const response = yield fetch(`https://saavn.dev/api/search/songs?query=${encodeURIComponent(query)}&limit=${limit}`);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
const json = yield response.json();
|
|
29
|
+
return ((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a.results) || [];
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error("TunzoPlayerAPI Error:", error);
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.TunzoPlayerAPI = TunzoPlayerAPI;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare class Player {
|
|
2
|
+
private static audio;
|
|
3
|
+
private static currentSong;
|
|
4
|
+
private static currentIndex;
|
|
5
|
+
private static isPlaying;
|
|
6
|
+
private static currentTime;
|
|
7
|
+
private static duration;
|
|
8
|
+
private static isShuffle;
|
|
9
|
+
private static queue;
|
|
10
|
+
private static playlist;
|
|
11
|
+
private static selectedQuality;
|
|
12
|
+
/** Initialize with playlist and quality */
|
|
13
|
+
static initialize(playlist: any[], quality?: number): void;
|
|
14
|
+
static play(song: any, index?: number): void;
|
|
15
|
+
static pause(): void;
|
|
16
|
+
static resume(): void;
|
|
17
|
+
static togglePlayPause(): void;
|
|
18
|
+
static next(): void;
|
|
19
|
+
static prev(): void;
|
|
20
|
+
static seek(seconds: number): void;
|
|
21
|
+
static autoNext(): void;
|
|
22
|
+
static playRandom(): void;
|
|
23
|
+
static toggleShuffle(): void;
|
|
24
|
+
static addToQueue(song: any): void;
|
|
25
|
+
static removeFromQueue(index: number): void;
|
|
26
|
+
static reorderQueue(from: number, to: number): void;
|
|
27
|
+
static getCurrentTime(): number;
|
|
28
|
+
static getDuration(): number;
|
|
29
|
+
static formatTime(time: number): string;
|
|
30
|
+
static isPlayingSong(): boolean;
|
|
31
|
+
static getCurrentSong(): any;
|
|
32
|
+
static setQuality(index: number): void;
|
|
33
|
+
static getQueue(): any[];
|
|
34
|
+
static getPlaylist(): any[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Player = void 0;
|
|
4
|
+
class Player {
|
|
5
|
+
/** Initialize with playlist and quality */
|
|
6
|
+
static initialize(playlist, quality = 3) {
|
|
7
|
+
this.playlist = playlist;
|
|
8
|
+
this.selectedQuality = quality;
|
|
9
|
+
}
|
|
10
|
+
static play(song, index = 0) {
|
|
11
|
+
var _a;
|
|
12
|
+
if (!song || !song.downloadUrl)
|
|
13
|
+
return;
|
|
14
|
+
this.currentSong = song;
|
|
15
|
+
this.currentIndex = index;
|
|
16
|
+
this.audio.src = ((_a = song.downloadUrl[this.selectedQuality]) === null || _a === void 0 ? void 0 : _a.url) || '';
|
|
17
|
+
this.audio.play();
|
|
18
|
+
this.isPlaying = true;
|
|
19
|
+
// Set duration
|
|
20
|
+
this.audio.onloadedmetadata = () => {
|
|
21
|
+
this.duration = this.audio.duration;
|
|
22
|
+
};
|
|
23
|
+
// Set current time
|
|
24
|
+
this.audio.ontimeupdate = () => {
|
|
25
|
+
this.currentTime = this.audio.currentTime;
|
|
26
|
+
};
|
|
27
|
+
// Auto-play next song
|
|
28
|
+
this.audio.onended = () => {
|
|
29
|
+
this.autoNext();
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
static pause() {
|
|
33
|
+
this.audio.pause();
|
|
34
|
+
this.isPlaying = false;
|
|
35
|
+
}
|
|
36
|
+
static resume() {
|
|
37
|
+
this.audio.play();
|
|
38
|
+
this.isPlaying = true;
|
|
39
|
+
}
|
|
40
|
+
static togglePlayPause() {
|
|
41
|
+
if (this.isPlaying) {
|
|
42
|
+
this.pause();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
this.resume();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
static next() {
|
|
49
|
+
if (this.queue.length > 0) {
|
|
50
|
+
const nextQueued = this.queue.shift();
|
|
51
|
+
const index = this.playlist.findIndex(s => s.id === nextQueued.id);
|
|
52
|
+
this.play(nextQueued, index);
|
|
53
|
+
}
|
|
54
|
+
else if (this.isShuffle) {
|
|
55
|
+
this.playRandom();
|
|
56
|
+
}
|
|
57
|
+
else if (this.currentIndex < this.playlist.length - 1) {
|
|
58
|
+
this.play(this.playlist[this.currentIndex + 1], this.currentIndex + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
static prev() {
|
|
62
|
+
if (this.currentIndex > 0) {
|
|
63
|
+
this.play(this.playlist[this.currentIndex - 1], this.currentIndex - 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
static seek(seconds) {
|
|
67
|
+
this.audio.currentTime = seconds;
|
|
68
|
+
}
|
|
69
|
+
static autoNext() {
|
|
70
|
+
this.next();
|
|
71
|
+
}
|
|
72
|
+
static playRandom() {
|
|
73
|
+
if (this.playlist.length <= 1)
|
|
74
|
+
return;
|
|
75
|
+
let randomIndex;
|
|
76
|
+
do {
|
|
77
|
+
randomIndex = Math.floor(Math.random() * this.playlist.length);
|
|
78
|
+
} while (randomIndex === this.currentIndex);
|
|
79
|
+
this.play(this.playlist[randomIndex], randomIndex);
|
|
80
|
+
}
|
|
81
|
+
static toggleShuffle() {
|
|
82
|
+
this.isShuffle = !this.isShuffle;
|
|
83
|
+
}
|
|
84
|
+
static addToQueue(song) {
|
|
85
|
+
if (!this.queue.some(q => q.id === song.id)) {
|
|
86
|
+
this.queue.push(song);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
static removeFromQueue(index) {
|
|
90
|
+
this.queue.splice(index, 1);
|
|
91
|
+
}
|
|
92
|
+
static reorderQueue(from, to) {
|
|
93
|
+
const item = this.queue.splice(from, 1)[0];
|
|
94
|
+
this.queue.splice(to, 0, item);
|
|
95
|
+
}
|
|
96
|
+
static getCurrentTime() {
|
|
97
|
+
return this.currentTime;
|
|
98
|
+
}
|
|
99
|
+
static getDuration() {
|
|
100
|
+
return this.duration;
|
|
101
|
+
}
|
|
102
|
+
static formatTime(time) {
|
|
103
|
+
const minutes = Math.floor(time / 60);
|
|
104
|
+
const seconds = Math.floor(time % 60);
|
|
105
|
+
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
|
106
|
+
}
|
|
107
|
+
static isPlayingSong() {
|
|
108
|
+
return this.isPlaying;
|
|
109
|
+
}
|
|
110
|
+
static getCurrentSong() {
|
|
111
|
+
return this.currentSong;
|
|
112
|
+
}
|
|
113
|
+
static setQuality(index) {
|
|
114
|
+
this.selectedQuality = index;
|
|
115
|
+
}
|
|
116
|
+
static getQueue() {
|
|
117
|
+
return this.queue;
|
|
118
|
+
}
|
|
119
|
+
static getPlaylist() {
|
|
120
|
+
return this.playlist;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.Player = Player;
|
|
124
|
+
Player.audio = new Audio();
|
|
125
|
+
Player.currentSong = null;
|
|
126
|
+
Player.currentIndex = 0;
|
|
127
|
+
Player.isPlaying = false;
|
|
128
|
+
Player.currentTime = 0;
|
|
129
|
+
Player.duration = 0;
|
|
130
|
+
Player.isShuffle = true;
|
|
131
|
+
Player.queue = [];
|
|
132
|
+
Player.playlist = [];
|
|
133
|
+
Player.selectedQuality = 3;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface StreamQuality {
|
|
2
|
+
value: number;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class StreamSettings {
|
|
6
|
+
static readonly qualityOptions: StreamQuality[];
|
|
7
|
+
static readonly qualityValueKey = "qualityValue";
|
|
8
|
+
static readonly qualityLabelKey = "qualityLabel";
|
|
9
|
+
/**
|
|
10
|
+
* Loads stream quality from localStorage
|
|
11
|
+
*/
|
|
12
|
+
static loadQuality(): StreamQuality;
|
|
13
|
+
/**
|
|
14
|
+
* Saves stream quality to localStorage
|
|
15
|
+
* @param value Index of quality option
|
|
16
|
+
* @param label Display label of selected quality
|
|
17
|
+
*/
|
|
18
|
+
static saveQuality(value: number, label: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Updates quality using value index only
|
|
21
|
+
* @param value Stream quality index (0 to 4)
|
|
22
|
+
*/
|
|
23
|
+
static updateQuality(value: number): StreamQuality;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the label for a given value
|
|
26
|
+
* @param value Quality value index
|
|
27
|
+
*/
|
|
28
|
+
static getLabel(value: number): string;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the available quality options
|
|
31
|
+
*/
|
|
32
|
+
static getOptions(): StreamQuality[];
|
|
33
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StreamSettings = void 0;
|
|
4
|
+
class StreamSettings {
|
|
5
|
+
/**
|
|
6
|
+
* Loads stream quality from localStorage
|
|
7
|
+
*/
|
|
8
|
+
static loadQuality() {
|
|
9
|
+
var _a;
|
|
10
|
+
const value = parseInt(localStorage.getItem(this.qualityValueKey) || '3');
|
|
11
|
+
const label = localStorage.getItem(this.qualityLabelKey) ||
|
|
12
|
+
((_a = this.qualityOptions.find(q => q.value === value)) === null || _a === void 0 ? void 0 : _a.label) ||
|
|
13
|
+
'High (160kbps)';
|
|
14
|
+
return { value, label };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Saves stream quality to localStorage
|
|
18
|
+
* @param value Index of quality option
|
|
19
|
+
* @param label Display label of selected quality
|
|
20
|
+
*/
|
|
21
|
+
static saveQuality(value, label) {
|
|
22
|
+
localStorage.setItem(this.qualityValueKey, value.toString());
|
|
23
|
+
localStorage.setItem(this.qualityLabelKey, label);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Updates quality using value index only
|
|
27
|
+
* @param value Stream quality index (0 to 4)
|
|
28
|
+
*/
|
|
29
|
+
static updateQuality(value) {
|
|
30
|
+
const quality = this.qualityOptions.find(q => q.value === value);
|
|
31
|
+
if (!quality)
|
|
32
|
+
throw new Error('Invalid quality value');
|
|
33
|
+
this.saveQuality(quality.value, quality.label);
|
|
34
|
+
return quality;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the label for a given value
|
|
38
|
+
* @param value Quality value index
|
|
39
|
+
*/
|
|
40
|
+
static getLabel(value) {
|
|
41
|
+
var _a;
|
|
42
|
+
return ((_a = this.qualityOptions.find(q => q.value === value)) === null || _a === void 0 ? void 0 : _a.label) || '';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns the available quality options
|
|
46
|
+
*/
|
|
47
|
+
static getOptions() {
|
|
48
|
+
return this.qualityOptions;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.StreamSettings = StreamSettings;
|
|
52
|
+
// All available stream quality options
|
|
53
|
+
StreamSettings.qualityOptions = [
|
|
54
|
+
{ value: 0, label: 'Very Low (12kbps)' },
|
|
55
|
+
{ value: 1, label: 'Low (48kbps)' },
|
|
56
|
+
{ value: 2, label: 'Medium (96kbps)' },
|
|
57
|
+
{ value: 3, label: 'High (160kbps)' },
|
|
58
|
+
{ value: 4, label: 'Ultra (320kbps)' },
|
|
59
|
+
];
|
|
60
|
+
StreamSettings.qualityValueKey = 'qualityValue';
|
|
61
|
+
StreamSettings.qualityLabelKey = 'qualityLabel';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class ThemeManager {
|
|
2
|
+
private static readonly key;
|
|
3
|
+
private static readonly darkClass;
|
|
4
|
+
/**
|
|
5
|
+
* Initializes the theme based on system preference or stored value
|
|
6
|
+
*/
|
|
7
|
+
static initialize(): void;
|
|
8
|
+
/**
|
|
9
|
+
* Applies dark/light theme class
|
|
10
|
+
* @param shouldAdd Whether to add dark class
|
|
11
|
+
*/
|
|
12
|
+
static apply(shouldAdd: boolean): void;
|
|
13
|
+
/**
|
|
14
|
+
* Toggles current theme between light and dark
|
|
15
|
+
*/
|
|
16
|
+
static toggle(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Returns current theme value
|
|
19
|
+
*/
|
|
20
|
+
static getCurrent(): 'light' | 'dark';
|
|
21
|
+
/**
|
|
22
|
+
* Returns true if dark mode is active
|
|
23
|
+
*/
|
|
24
|
+
static isDark(): boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ThemeManager = void 0;
|
|
4
|
+
class ThemeManager {
|
|
5
|
+
/**
|
|
6
|
+
* Initializes the theme based on system preference or stored value
|
|
7
|
+
*/
|
|
8
|
+
static initialize() {
|
|
9
|
+
const stored = localStorage.getItem(this.key);
|
|
10
|
+
if (stored !== null) {
|
|
11
|
+
const shouldAdd = stored === 'dark';
|
|
12
|
+
this.apply(shouldAdd);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
|
16
|
+
this.apply(prefersDark.matches);
|
|
17
|
+
// Save for future
|
|
18
|
+
localStorage.setItem(this.key, prefersDark.matches ? 'dark' : 'light');
|
|
19
|
+
}
|
|
20
|
+
// Optional: listen to system change
|
|
21
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
|
|
22
|
+
this.apply(event.matches);
|
|
23
|
+
localStorage.setItem(this.key, event.matches ? 'dark' : 'light');
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Applies dark/light theme class
|
|
28
|
+
* @param shouldAdd Whether to add dark class
|
|
29
|
+
*/
|
|
30
|
+
static apply(shouldAdd) {
|
|
31
|
+
document.documentElement.classList.toggle(this.darkClass, shouldAdd);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Toggles current theme between light and dark
|
|
35
|
+
*/
|
|
36
|
+
static toggle() {
|
|
37
|
+
const isDark = document.documentElement.classList.contains(this.darkClass);
|
|
38
|
+
const newMode = isDark ? 'light' : 'dark';
|
|
39
|
+
this.apply(!isDark);
|
|
40
|
+
localStorage.setItem(this.key, newMode);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns current theme value
|
|
44
|
+
*/
|
|
45
|
+
static getCurrent() {
|
|
46
|
+
return document.documentElement.classList.contains(this.darkClass) ? 'dark' : 'light';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if dark mode is active
|
|
50
|
+
*/
|
|
51
|
+
static isDark() {
|
|
52
|
+
return this.getCurrent() === 'dark';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.ThemeManager = ThemeManager;
|
|
56
|
+
ThemeManager.key = 'themePalette'; // localStorage key
|
|
57
|
+
ThemeManager.darkClass = 'ion-palette-dark'; // CSS class to toggle
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./core/player"), exports);
|
|
18
|
+
__exportStar(require("./core/api-calles"), exports);
|
|
19
|
+
__exportStar(require("./core/settings"), exports);
|
|
20
|
+
__exportStar(require("./core/theme"), exports);
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tunzo-player",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A music playback service for Angular and Ionic apps with native audio control support.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["music", "player", "angular", "ionic", "audio"],
|
|
11
|
+
"author": "Kulasekaran",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class TunzoPlayerAPI {
|
|
2
|
+
/**
|
|
3
|
+
* Search for songs using the saavn.dev API
|
|
4
|
+
* @param query Search keyword (e.g., artist name, song name)
|
|
5
|
+
* @param limit Number of results to return (default: 250)
|
|
6
|
+
* @returns Array of song result objects
|
|
7
|
+
*/
|
|
8
|
+
async searchSongs(query: string, limit: number = 250): Promise<any[]> {
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(
|
|
11
|
+
`https://saavn.dev/api/search/songs?query=${encodeURIComponent(query)}&limit=${limit}`
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const json = await response.json();
|
|
19
|
+
return json?.data?.results || [];
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error("TunzoPlayerAPI Error:", error);
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export class Player {
|
|
2
|
+
private static audio = new Audio();
|
|
3
|
+
private static currentSong: any = null;
|
|
4
|
+
private static currentIndex = 0;
|
|
5
|
+
private static isPlaying = false;
|
|
6
|
+
private static currentTime = 0;
|
|
7
|
+
private static duration = 0;
|
|
8
|
+
private static isShuffle = true;
|
|
9
|
+
private static queue: any[] = [];
|
|
10
|
+
private static playlist: any[] = [];
|
|
11
|
+
private static selectedQuality = 3;
|
|
12
|
+
|
|
13
|
+
/** Initialize with playlist and quality */
|
|
14
|
+
static initialize(playlist: any[], quality = 3) {
|
|
15
|
+
this.playlist = playlist;
|
|
16
|
+
this.selectedQuality = quality;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static play(song: any, index: number = 0) {
|
|
20
|
+
if (!song || !song.downloadUrl) return;
|
|
21
|
+
|
|
22
|
+
this.currentSong = song;
|
|
23
|
+
this.currentIndex = index;
|
|
24
|
+
this.audio.src = song.downloadUrl[this.selectedQuality]?.url || '';
|
|
25
|
+
this.audio.play();
|
|
26
|
+
this.isPlaying = true;
|
|
27
|
+
|
|
28
|
+
// Set duration
|
|
29
|
+
this.audio.onloadedmetadata = () => {
|
|
30
|
+
this.duration = this.audio.duration;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Set current time
|
|
34
|
+
this.audio.ontimeupdate = () => {
|
|
35
|
+
this.currentTime = this.audio.currentTime;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Auto-play next song
|
|
39
|
+
this.audio.onended = () => {
|
|
40
|
+
this.autoNext();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static pause() {
|
|
45
|
+
this.audio.pause();
|
|
46
|
+
this.isPlaying = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static resume() {
|
|
50
|
+
this.audio.play();
|
|
51
|
+
this.isPlaying = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static togglePlayPause() {
|
|
55
|
+
if (this.isPlaying) {
|
|
56
|
+
this.pause();
|
|
57
|
+
} else {
|
|
58
|
+
this.resume();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static next() {
|
|
63
|
+
if (this.queue.length > 0) {
|
|
64
|
+
const nextQueued = this.queue.shift();
|
|
65
|
+
const index = this.playlist.findIndex(s => s.id === nextQueued.id);
|
|
66
|
+
this.play(nextQueued, index);
|
|
67
|
+
} else if (this.isShuffle) {
|
|
68
|
+
this.playRandom();
|
|
69
|
+
} else if (this.currentIndex < this.playlist.length - 1) {
|
|
70
|
+
this.play(this.playlist[this.currentIndex + 1], this.currentIndex + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static prev() {
|
|
75
|
+
if (this.currentIndex > 0) {
|
|
76
|
+
this.play(this.playlist[this.currentIndex - 1], this.currentIndex - 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static seek(seconds: number) {
|
|
81
|
+
this.audio.currentTime = seconds;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static autoNext() {
|
|
85
|
+
this.next();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static playRandom() {
|
|
89
|
+
if (this.playlist.length <= 1) return;
|
|
90
|
+
|
|
91
|
+
let randomIndex;
|
|
92
|
+
do {
|
|
93
|
+
randomIndex = Math.floor(Math.random() * this.playlist.length);
|
|
94
|
+
} while (randomIndex === this.currentIndex);
|
|
95
|
+
|
|
96
|
+
this.play(this.playlist[randomIndex], randomIndex);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static toggleShuffle() {
|
|
100
|
+
this.isShuffle = !this.isShuffle;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static addToQueue(song: any) {
|
|
104
|
+
if (!this.queue.some(q => q.id === song.id)) {
|
|
105
|
+
this.queue.push(song);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static removeFromQueue(index: number) {
|
|
110
|
+
this.queue.splice(index, 1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static reorderQueue(from: number, to: number) {
|
|
114
|
+
const item = this.queue.splice(from, 1)[0];
|
|
115
|
+
this.queue.splice(to, 0, item);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static getCurrentTime(): number {
|
|
119
|
+
return this.currentTime;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static getDuration(): number {
|
|
123
|
+
return this.duration;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static formatTime(time: number): string {
|
|
127
|
+
const minutes = Math.floor(time / 60);
|
|
128
|
+
const seconds = Math.floor(time % 60);
|
|
129
|
+
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static isPlayingSong(): boolean {
|
|
133
|
+
return this.isPlaying;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static getCurrentSong(): any {
|
|
137
|
+
return this.currentSong;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static setQuality(index: number) {
|
|
141
|
+
this.selectedQuality = index;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static getQueue(): any[] {
|
|
145
|
+
return this.queue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static getPlaylist(): any[] {
|
|
149
|
+
return this.playlist;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface StreamQuality {
|
|
2
|
+
value: number;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class StreamSettings {
|
|
7
|
+
// All available stream quality options
|
|
8
|
+
static readonly qualityOptions: StreamQuality[] = [
|
|
9
|
+
{ value: 0, label: 'Very Low (12kbps)' },
|
|
10
|
+
{ value: 1, label: 'Low (48kbps)' },
|
|
11
|
+
{ value: 2, label: 'Medium (96kbps)' },
|
|
12
|
+
{ value: 3, label: 'High (160kbps)' },
|
|
13
|
+
{ value: 4, label: 'Ultra (320kbps)' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
static readonly qualityValueKey = 'qualityValue';
|
|
17
|
+
static readonly qualityLabelKey = 'qualityLabel';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loads stream quality from localStorage
|
|
21
|
+
*/
|
|
22
|
+
static loadQuality(): StreamQuality {
|
|
23
|
+
const value = parseInt(localStorage.getItem(this.qualityValueKey) || '3');
|
|
24
|
+
const label =
|
|
25
|
+
localStorage.getItem(this.qualityLabelKey) ||
|
|
26
|
+
this.qualityOptions.find(q => q.value === value)?.label ||
|
|
27
|
+
'High (160kbps)';
|
|
28
|
+
|
|
29
|
+
return { value, label };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Saves stream quality to localStorage
|
|
34
|
+
* @param value Index of quality option
|
|
35
|
+
* @param label Display label of selected quality
|
|
36
|
+
*/
|
|
37
|
+
static saveQuality(value: number, label: string) {
|
|
38
|
+
localStorage.setItem(this.qualityValueKey, value.toString());
|
|
39
|
+
localStorage.setItem(this.qualityLabelKey, label);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Updates quality using value index only
|
|
44
|
+
* @param value Stream quality index (0 to 4)
|
|
45
|
+
*/
|
|
46
|
+
static updateQuality(value: number): StreamQuality {
|
|
47
|
+
const quality = this.qualityOptions.find(q => q.value === value);
|
|
48
|
+
if (!quality) throw new Error('Invalid quality value');
|
|
49
|
+
|
|
50
|
+
this.saveQuality(quality.value, quality.label);
|
|
51
|
+
return quality;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns the label for a given value
|
|
56
|
+
* @param value Quality value index
|
|
57
|
+
*/
|
|
58
|
+
static getLabel(value: number): string {
|
|
59
|
+
return this.qualityOptions.find(q => q.value === value)?.label || '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns the available quality options
|
|
64
|
+
*/
|
|
65
|
+
static getOptions(): StreamQuality[] {
|
|
66
|
+
return this.qualityOptions;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export class ThemeManager {
|
|
2
|
+
private static readonly key = 'themePalette'; // localStorage key
|
|
3
|
+
private static readonly darkClass = 'ion-palette-dark'; // CSS class to toggle
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initializes the theme based on system preference or stored value
|
|
7
|
+
*/
|
|
8
|
+
static initialize() {
|
|
9
|
+
const stored = localStorage.getItem(this.key);
|
|
10
|
+
|
|
11
|
+
if (stored !== null) {
|
|
12
|
+
const shouldAdd = stored === 'dark';
|
|
13
|
+
this.apply(shouldAdd);
|
|
14
|
+
} else {
|
|
15
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
|
16
|
+
this.apply(prefersDark.matches);
|
|
17
|
+
|
|
18
|
+
// Save for future
|
|
19
|
+
localStorage.setItem(this.key, prefersDark.matches ? 'dark' : 'light');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Optional: listen to system change
|
|
23
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
|
|
24
|
+
this.apply(event.matches);
|
|
25
|
+
localStorage.setItem(this.key, event.matches ? 'dark' : 'light');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Applies dark/light theme class
|
|
31
|
+
* @param shouldAdd Whether to add dark class
|
|
32
|
+
*/
|
|
33
|
+
static apply(shouldAdd: boolean) {
|
|
34
|
+
document.documentElement.classList.toggle(this.darkClass, shouldAdd);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Toggles current theme between light and dark
|
|
39
|
+
*/
|
|
40
|
+
static toggle() {
|
|
41
|
+
const isDark = document.documentElement.classList.contains(this.darkClass);
|
|
42
|
+
const newMode = isDark ? 'light' : 'dark';
|
|
43
|
+
this.apply(!isDark);
|
|
44
|
+
localStorage.setItem(this.key, newMode);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns current theme value
|
|
49
|
+
*/
|
|
50
|
+
static getCurrent(): 'light' | 'dark' {
|
|
51
|
+
return document.documentElement.classList.contains(this.darkClass) ? 'dark' : 'light';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true if dark mode is active
|
|
56
|
+
*/
|
|
57
|
+
static isDark(): boolean {
|
|
58
|
+
return this.getCurrent() === 'dark';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
/* Output settings */
|
|
4
|
+
"outDir": "./dist", // output compiled files here
|
|
5
|
+
"rootDir": "./src", // source files are inside src/
|
|
6
|
+
"declaration": true, // generate .d.ts types
|
|
7
|
+
"declarationDir": "./dist", // put .d.ts files in dist too
|
|
8
|
+
|
|
9
|
+
/* Module & Target */
|
|
10
|
+
"module": "CommonJS", // compatible with Node/npm
|
|
11
|
+
"target": "ES2016", // or ES2020 if you prefer
|
|
12
|
+
|
|
13
|
+
/* Type Compatibility */
|
|
14
|
+
"esModuleInterop": true, // allow default imports from CommonJS
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true // speed up builds, safe for libs
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"] // only compile the src folder
|
|
20
|
+
}
|