Last active
September 19, 2026 12:52
-
-
Save HemanthJabalpuri/79a05fbbdee5be2a1760977b5f18ea95 to your computer and use it in GitHub Desktop.
JioSaavn plugin for MusicFree app by maotoumao
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Init | |
| const LOG_LEVELS = { none: 0, error: 1, warn: 2, info: 3, debug: 4 }; | |
| function getUserVars() { | |
| try { | |
| return (env && typeof env.getUserVariables === 'function') ? env.getUserVariables() : {}; | |
| } catch (_) { | |
| return {}; | |
| } | |
| } | |
| function log(level, ...args) { | |
| const vars = getUserVars(); | |
| const logLevel = vars.logLevel || 'error'; | |
| const currentLevel = LOG_LEVELS[logLevel] !== undefined ? LOG_LEVELS[logLevel] : LOG_LEVELS.error; | |
| const msgLevel = LOG_LEVELS[level] || 0; | |
| if (msgLevel <= currentLevel) { | |
| const prefix = '[JioSaavn]'; | |
| const method = level === 'error' ? console.error : | |
| level === 'warn' ? console.warn : | |
| console.log; | |
| method(prefix, ...args); | |
| } | |
| } | |
| // get language settings | |
| // not working for now | |
| function getLanguageHeader() { | |
| try { | |
| const vars = getUserVars(); | |
| const languages = vars.languages || ''; | |
| log('debug', '[getLanguageHeader] Languages are...', languages); | |
| if (languages) { | |
| // en-US,en; geo=17.3850,78.4867,IN,Telangana,Hyderabad,500001; mm_latlong=17.3850,78.4867; DL=english; | |
| return { 'Cookie': `L=${languages}` }; | |
| } | |
| } catch (_) {} | |
| return {}; | |
| } | |
| const SEARCH_LIMIT = 10; | |
| const RADIO_DEFAULT_COUNT = 20; | |
| async function callAPI(call, extraParams = {}, extraHeaders = {}) { | |
| log('debug', '[callAPI] Calling', call, 'with extraParams:', extraParams, 'with extraHeaders:', extraHeaders); | |
| const params = { | |
| __call: call, | |
| _format: 'json', | |
| _marker: 0, | |
| api_version: 4, | |
| ctx: 'web6dot0', | |
| ...extraParams | |
| }; | |
| const qs = new URLSearchParams(params).toString(); | |
| const url = `https://www.jiosaavn.com/api.php?${qs}`; | |
| const res = await fetch(url, { headers: extraHeaders }); | |
| log('debug', '[callAPI] Response status:', res.status); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| return res.json(); | |
| } | |
| function searchSongsAPI(q, p, n) { | |
| log('debug', '[searchSongsAPI] query:', q, 'page:', p, 'limit:', n); | |
| return callAPI('search.getResults', { q, p, n }); | |
| } | |
| function searchSheetsAPI(type, q, p, n) { | |
| log('debug', '[searchSheetsAPI] query:', q, 'page:', p, 'limit:', n); | |
| return callAPI(type === 'album' ? 'search.getAlbumResults' : 'search.getPlaylistResults', { q, p, n }); | |
| } | |
| function searchArtistsAPI(q, p, n) { | |
| log('debug', '[searchArtistsAPI] query:', q, 'page:', p, 'limit:', n); | |
| return callAPI('search.getArtistResults', { q, p, n }); | |
| } | |
| function getSongAPI(token) { | |
| log('debug', '[getSongAPI] token:', token); | |
| return callAPI('webapi.get', { | |
| token, | |
| type: 'song', | |
| includeMetaTags: 0 | |
| }); | |
| } | |
| function getLyricsAPI(token) { | |
| log('debug', '[getLyricsAPI] token:', token); | |
| return callAPI('webapi.get', { | |
| token, | |
| type: 'lyrics' | |
| }); | |
| } | |
| function getSheetAPI(type, token, p, n) { | |
| log('debug', '[getSheetAPI] type:', type, 'token:', token); | |
| const params = { | |
| token, | |
| type, | |
| includeMetaTags: 0 | |
| }; | |
| if (type === 'playlist') { | |
| params.p = p; | |
| params.n = n; | |
| } | |
| return callAPI('webapi.get', params); | |
| } | |
| function getArtistSongsAPI(artistId, page, category) { | |
| // Artist pagination starts from 0, so convert 1‑based page to 0‑based | |
| const zeroBasedPage = Math.max(0, (page || 1) - 1); | |
| log('debug', '[getArtistSongsAPI] artistId:', artistId, 'page:', page, 'zeroBasedPage:', zeroBasedPage); | |
| return callAPI('artist.getArtistMoreSong', { | |
| artistId, | |
| page: zeroBasedPage, | |
| category: category || 'popular', | |
| sort_order: category === 'latest' ? 'desc' : 'asc' | |
| }); | |
| } | |
| function getArtistAlbumsAPI(artistId, page, category) { | |
| const zeroBasedPage = Math.max(0, (page || 1) - 1); | |
| log('debug', '[getArtistAlbumsAPI] artistId:', artistId, 'page:', page, 'zeroBasedPage:', zeroBasedPage); | |
| return callAPI('artist.getArtistMoreAlbum', { | |
| artistId, | |
| page: zeroBasedPage, | |
| category: category || 'popular', | |
| sort_order: category === 'latest' ? 'desc' : 'asc' | |
| }); | |
| } | |
| function createRadioStationAPI(stationType, params) { | |
| const apiCall = stationType === 'featured' ? 'webradio.createFeaturedStation' : 'webradio.createArtistStation'; | |
| const payload = { language: params.language || 'hindi', pid: '', name: params.name, mode: '', artistid: '' }; | |
| if (stationType === 'artist') { | |
| payload.query = params.query || ''; | |
| } else { | |
| payload.query = ''; | |
| } | |
| log('debug', '[createRadioStationAPI] Calling', apiCall, 'with payload:', payload); | |
| return callAPI(apiCall, payload, getLanguageHeader()); | |
| } | |
| function getRadioSongsAPI(stationId, count, page) { | |
| log('debug', '[getRadioSongsAPI] Fetching songs for station:', stationId, 'page:', page); | |
| // TODO: we are not using page var, let's think about it later | |
| return callAPI('webradio.getSong', { | |
| stationid: stationId, | |
| k: count || RADIO_DEFAULT_COUNT, | |
| next: 1 | |
| }, getLanguageHeader()); | |
| } | |
| const CryptoJs = require("crypto-js"); | |
| const DESKEY = CryptoJs.enc.Utf8.parse('38346591'); | |
| const DES_OPTIONS = { | |
| mode: CryptoJs.mode.ECB, | |
| padding: CryptoJs.pad.Pkcs7 | |
| }; | |
| function decryptMediaUrl(encrypted) { | |
| log('debug', '[decryptMediaUrl] decrypting length:', encrypted.length); | |
| const decrypted = CryptoJs.DES.decrypt(encrypted, DESKEY, DES_OPTIONS); | |
| log('debug', '[decryptMediaUrl] decrypted (first 50 chars):', decrypted.toString(CryptoJs.enc.Utf8).slice(0, 50)); | |
| return decrypted.toString(CryptoJs.enc.Utf8); | |
| } | |
| function decode(text) { | |
| if (!text) return ''; | |
| let result = text; | |
| let previous; | |
| do { | |
| previous = result; | |
| result = result.replace(/&/g, '&').replace(/'/g, "'").replace(/"/g, '"'); | |
| } while (result !== previous); | |
| return result; | |
| } | |
| function extractTokenFromUrl(url) { | |
| const parts = url.split('/'); | |
| return parts[parts.length - 1]; | |
| } | |
| function formatSong(song) { | |
| log('debug', '[formatSong] formatting:', song.title, 'id:', song.id); | |
| const moreInfo = song.more_info || {}; | |
| const subtitle = decode(song.subtitle || ''); | |
| const artist = subtitle.includes(' - ') ? subtitle.split(' - ')[0] : subtitle; | |
| return { | |
| id: extractTokenFromUrl(song.perma_url), | |
| title: decode(song.title), | |
| artist: artist || 'Unknown Artist', | |
| album: decode(moreInfo.album || ''), | |
| artwork: (song.image || '').replace(/\d+x\d+\.jpg$/, '500x500.jpg'), | |
| duration: parseInt(moreInfo.duration) || 0, | |
| _encryptedUrl: moreInfo.encrypted_media_url, | |
| _hasLyrics: moreInfo.has_lyrics === 'true' || moreInfo.has_lyrics === true | |
| }; | |
| } | |
| function formatSheet(sheet) { | |
| const result = { | |
| id: extractTokenFromUrl(sheet.perma_url), | |
| title: decode(sheet.title), | |
| artist: decode(sheet.subtitle || ''), | |
| artwork: sheet.image || '', | |
| type: sheet.type // custom key | |
| }; | |
| // If it's a song or radio station, store the raw data for later use in getSheetInfo | |
| if (result.type === 'song' || result.type === 'radio_station') { | |
| result._raw = sheet; | |
| } | |
| return result; | |
| } | |
| function formatSheetTracks(data) { | |
| const tracks = data.list || []; | |
| log('debug', '[formatSheetTracks] tracks count:', tracks.length); | |
| return tracks.map(formatSong); | |
| } | |
| function formatArtist(artist) { | |
| return { | |
| id: artist.id, | |
| name: decode(artist.name), | |
| avatar: (artist.image || '').replace(/\d+x\d+\.jpg$/, '150x150.jpg') | |
| }; | |
| } | |
| let urlCache = null; // will hold { id, url } used by getMediaSource | |
| const QUALITY_MAP = { low: '48', standard: '96', high: '160', super: '320' }; | |
| function getMediaSource(musicItem, quality) { | |
| log('debug', '[getMediaSource] song:', musicItem.title, 'quality:', quality); | |
| if (urlCache === null || urlCache.id !== musicItem.id) { | |
| log('debug', '[getMediaSource] cache miss – decrypting'); | |
| const decrypted = decryptMediaUrl(musicItem._encryptedUrl); | |
| urlCache = { id: musicItem.id, url: decrypted }; | |
| } else { | |
| log('debug', '[getMediaSource] cache hit'); | |
| } | |
| const finalUrl = urlCache.url.replace(/_[0-9]+(?=\.)/, '_' + QUALITY_MAP[quality]); | |
| log('debug', '[getMediaSource] final URL:', finalUrl); | |
| return { url: finalUrl }; | |
| } | |
| async function getLyric(musicItem) { | |
| log('debug', '[getLyric] fetching lyrics for:', musicItem.title); | |
| if (!musicItem._hasLyrics) { | |
| log('debug', '[getLyric] no lyrics flag'); | |
| return { rawLrc: '' }; | |
| } | |
| const response = await getLyricsAPI(musicItem.id); | |
| const rawLyrics = response?.lyrics?.lyrics; | |
| log('debug', '[getLyric] lyrics length:', rawLyrics?.length); | |
| return { rawLrc: '[00:00.00] ' + rawLyrics.replace(/<br>/g, '\n') }; | |
| } | |
| const radioStationCache = { | |
| sheetId: null, | |
| stationId: null | |
| }; | |
| async function getRadioStationInfo(radioSheetItem, page) { | |
| log('debug', '[getRadioStationInfo] Processing radio station:', radioSheetItem.title); | |
| const moreInfo = radioSheetItem._raw.more_info || {}; | |
| const stationType = moreInfo.featured_station_type || 'featured'; | |
| const params = { | |
| language: moreInfo.language || 'hindi', | |
| name: encodeURIComponent(radioSheetItem.title), | |
| query: encodeURIComponent(moreInfo.query) || '' | |
| }; | |
| // Check if we have a cached station ID for this sheet | |
| let stationId; | |
| if (radioStationCache.sheetId === radioSheetItem.id && radioStationCache.stationId) { | |
| stationId = radioStationCache.stationId; | |
| log('debug', '[getRadioStationInfo] Using cached stationId:', stationId); | |
| } else if (page === 1) { | |
| // Create the station | |
| const createResponse = await createRadioStationAPI(stationType, params); | |
| stationId = createResponse?.stationid; | |
| if (!stationId) { | |
| log('warn', '[getRadioStationInfo] No stationid in response:', createResponse); | |
| return { isEnd: true, musicList: [] }; | |
| } | |
| // Update cache | |
| radioStationCache.sheetId = radioSheetItem.id; | |
| radioStationCache.stationId = stationId; | |
| log('debug', '[getRadioStationInfo] Created and cached stationId:', stationId); | |
| } else { | |
| // Cache missing but page > 1 – log and return empty | |
| log('warn', '[getRadioStationInfo] Cache missing for page', page, ', returning empty'); | |
| return { isEnd: true, musicList: [] }; | |
| } | |
| // Fetch songs using the station ID | |
| const songsResponse = await getRadioSongsAPI(encodeURIComponent(stationId), RADIO_DEFAULT_COUNT, page); | |
| // Parse response: object with numeric keys, each containing { song: { ... } } | |
| const songs = []; | |
| for (const key of Object.keys(songsResponse)) { | |
| const item = songsResponse[key]; | |
| if (item && item.song) { | |
| songs.push(item.song); | |
| } | |
| } | |
| const isEnd = songs.length < RADIO_DEFAULT_COUNT; | |
| const musicList = songs.map(formatSong); | |
| log('debug', '[getRadioStationInfo] Returning', musicList.length, 'songs, isEnd:', isEnd); | |
| return { isEnd, musicList }; | |
| } | |
| async function getSheetInfo(sheetItem, page) { | |
| log('debug', '[getSheetInfo] type:', sheetItem.type, 'sheet:', sheetItem.title); | |
| // --- Radio station --- | |
| if (sheetItem.type === 'radio_station') { | |
| return getRadioStationInfo(sheetItem, page); | |
| } | |
| // --- Song sheet (single track) --- | |
| if (sheetItem.type === 'song') { | |
| log('debug', '[getSheetInfo] Song sheet detected, formatting single track'); | |
| return { | |
| isEnd: true, | |
| musicList: [ formatSong(sheetItem._raw) ] | |
| }; | |
| } | |
| // --- Album / Playlist --- | |
| const response = await getSheetAPI(sheetItem.type, sheetItem.id, page, 50); | |
| const tracks = formatSheetTracks(response); | |
| log('debug', '[getSheetInfo] tracks count:', tracks.length); | |
| const isEnd = sheetItem.type === 'playlist' ? tracks.length < 50 : true; | |
| return { isEnd, musicList: tracks }; | |
| } | |
| async function getArtistWorks(artistItem, page, type) { | |
| log('debug', '[getArtistWorks] artist:', artistItem.name, 'page:', page, 'type:', type); | |
| // Handle albums | |
| if (type === 'album') { | |
| const response = await getArtistAlbumsAPI(artistItem.id, page); | |
| const rawAlbums = response.topAlbums.albums || []; | |
| const albums = rawAlbums.map(formatSheet); | |
| log('debug', '[getArtistWorks] fetched', albums.length, 'albums'); | |
| const limit = 10; // adjust if API has different default | |
| return { | |
| isEnd: albums.length < limit, | |
| data: albums | |
| }; | |
| } | |
| // Handle songs | |
| const response = await getArtistSongsAPI(artistItem.id, page); | |
| const rawSongs = response.topSongs.songs || []; | |
| const songs = rawSongs.map(formatSong); | |
| log('debug', '[getArtistWorks] fetched', songs.length, 'songs'); | |
| const limit = 10; // adjust if API has different default | |
| return { | |
| isEnd: songs.length < limit, | |
| data: songs | |
| }; | |
| } | |
| const searchMap = { | |
| music: { | |
| call: (kw, p) => searchSongsAPI(kw, p, SEARCH_LIMIT), | |
| formatter: formatSong | |
| }, | |
| album: { | |
| call: (kw, p) => searchSheetsAPI('album', kw, p, SEARCH_LIMIT), | |
| formatter: formatSheet | |
| }, | |
| sheet: { | |
| call: (kw, p) => searchSheetsAPI('playlist', kw, p, SEARCH_LIMIT), | |
| formatter: formatSheet | |
| }, | |
| artist: { | |
| call: (kw, p) => searchArtistsAPI(kw, p, SEARCH_LIMIT), | |
| formatter: formatArtist | |
| } | |
| }; | |
| async function search(keyword, page, type) { | |
| log('debug', '[search] keyword:', keyword, 'page:', page, 'type:', type); | |
| const entry = searchMap[type]; | |
| if (!entry) return { isEnd: true, data: [] }; | |
| const response = await entry.call(keyword, page); | |
| const items = (response.results || []).map(entry.formatter); | |
| log('debug', '[search] items count:', items.length); | |
| return { | |
| isEnd: items.length < SEARCH_LIMIT, | |
| data: items | |
| }; | |
| } | |
| async function importMusicItem(url) { | |
| log('debug', '[importMusicItem] URL:', url); | |
| if (url.includes('/song/')) { | |
| const response = await getSongAPI(extractTokenFromUrl(url)); | |
| const song = formatSong(response.songs[0]); | |
| log('debug', '[importMusicItem] imported song:', song.title); | |
| return song; | |
| } | |
| throw new Error('URL is not a song link'); | |
| } | |
| async function importMusicSheet(url) { | |
| log('debug', '[importMusicSheet] URL:', url); | |
| let type; | |
| if (url.includes('/album/')) { | |
| type = 'album'; | |
| } else if (url.includes('/featured/') || url.includes('/playlist/')) { | |
| type = 'playlist'; | |
| } else { | |
| throw new Error('URL is not an album or playlist link'); | |
| } | |
| log('debug', '[importMusicSheet] Type:', type); | |
| const response = await getSheetAPI(type, extractTokenFromUrl(url), 1, 100); | |
| const tracks = formatSheetTracks(response); | |
| log('debug', '[importMusicSheet] tracks count:', tracks.length); | |
| return tracks; | |
| } | |
| async function getTopLists() { | |
| log('debug', '[getTopLists] Fetching trending content...'); | |
| // if we pass L param to Cookie, it should respond with that language specific content | |
| // but in MusicFree, it is not working like intended | |
| // keeping it for future reference | |
| const langHeader = getLanguageHeader(); | |
| const allItems = await callAPI('content.getCharts', {}, langHeader); | |
| log('debug', '[getTopLists] Received', allItems.length, 'items'); | |
| const playlists = allItems.filter(item => item.type === 'playlist'); | |
| log('debug', '[getTopLists] Playlists:', playlists.length); | |
| return [ | |
| { | |
| title: 'Top Charts', | |
| data: playlists.map(item => { | |
| const sheet = formatSheet(item); | |
| sheet.coverImg = sheet.artwork; // Ensure coverImg is present for top lists | |
| return sheet; | |
| }) | |
| } | |
| ]; | |
| } | |
| let launchDataCache = null; | |
| function hasSheets(data) { | |
| if (!Array.isArray(data)) return false; | |
| return data.some(item => ['album', 'playlist', 'song', 'radio_station'].includes(item.type)); | |
| } | |
| async function getRecommendSheetTags() { | |
| log('debug', '[getRecommendSheetTags] Called'); | |
| try { | |
| const langHeader = getLanguageHeader(); | |
| const data = await callAPI('webapi.getLaunchData', {}, langHeader); | |
| launchDataCache = data; | |
| const modules = data.modules || {}; | |
| const tags = []; | |
| for (const key of Object.keys(modules)) { | |
| const module = modules[key]; | |
| const topData = data[key]; | |
| if (hasSheets(topData)) { | |
| tags.push({ | |
| id: key, | |
| title: module.title || key | |
| }); | |
| } | |
| } | |
| log('debug', '[getRecommendSheetTags] Found', tags.length, 'tags'); | |
| return { | |
| data: [ | |
| { | |
| title: 'LaunchData', | |
| data: tags | |
| } | |
| ] | |
| }; | |
| } catch (e) { | |
| log('error', '[getRecommendSheetTags] Error:', e.message); | |
| return { data: [] }; | |
| } | |
| } | |
| async function getRecommendSheetsByTag(tag, page) { | |
| log('debug', '[getRecommendSheetsByTag] Tag:', tag, 'Page:', page); | |
| try { | |
| const langHeader = getLanguageHeader(); | |
| if (!launchDataCache) { | |
| log('debug', '[getRecommendSheetsByTag] Cache empty, fetching LaunchData...'); | |
| launchDataCache = await callAPI('webapi.getLaunchData', {}, langHeader); | |
| log('debug', '[getRecommendSheetsByTag] LaunchData fetched and cached.'); | |
| } | |
| // Handle "Default" tag | |
| let effectiveTagId = tag.id; | |
| if (!effectiveTagId || tag.title === 'Default') { | |
| log('debug', '[getRecommendSheetsByTag] "Default" tag detected, picking first module with sheets'); | |
| const modules = launchDataCache.modules || {}; | |
| for (const key of Object.keys(modules)) { | |
| const topData = launchDataCache[key]; | |
| if (hasSheets(topData)) { | |
| effectiveTagId = key; | |
| log('debug', '[getRecommendSheetsByTag] Mapped "Default" to:', effectiveTagId); | |
| break; | |
| } | |
| } | |
| if (!effectiveTagId || effectiveTagId === tag.id) { | |
| log('warn', '[getRecommendSheetsByTag] No module with sheets found, returning empty'); | |
| return { isEnd: true, data: [] }; | |
| } | |
| } | |
| const module = launchDataCache.modules?.[effectiveTagId]; | |
| if (!module) { | |
| log('warn', '[getRecommendSheetsByTag] Module not found:', effectiveTagId); | |
| return { isEnd: true, data: [] }; | |
| } | |
| let items = []; | |
| let isEnd = false; | |
| if (module.view_more && module.view_more.api) { | |
| const viewMore = module.view_more; | |
| const api = viewMore.api; | |
| const pageParam = viewMore.page_param || 'p'; | |
| const sizeParam = viewMore.size_param || 'n'; | |
| const defaultSize = viewMore.default_size || SEARCH_LIMIT; | |
| log('debug', '[getRecommendSheetsByTag] Fetching via view_more:', api); | |
| const response = await callAPI(api, { | |
| [pageParam]: page || 1, | |
| [sizeParam]: defaultSize | |
| }, langHeader); | |
| const responseData = response[effectiveTagId] || response.data || response.results || response.list || response; | |
| items = Array.isArray(responseData) ? responseData : []; | |
| isEnd = items.length < defaultSize; | |
| } else { | |
| log('debug', '[getRecommendSheetsByTag] Using cached:'); | |
| items = launchDataCache[effectiveTagId] || []; | |
| isEnd = true; | |
| } | |
| const sheets = items.filter(item => ['album', 'playlist', 'song', 'radio_station'].includes(item.type)); | |
| const formatted = sheets.map(formatSheet); | |
| return { isEnd, data: formatted }; | |
| } catch (e) { | |
| log('error', '[getRecommendSheetsByTag] Error:', e.message); | |
| return { isEnd: true, data: [] }; | |
| } | |
| } | |
| module.exports = { | |
| platform: 'JioSaavn', | |
| version: '0.3.7', | |
| author: 'Hemanth', | |
| description: ` | |
| # JioSaavn Plugin for [MusicFree](https://github.com/maotoumao/MusicFree) | |
| **Play songs, albums, playlists, and artists from India's JioSaavn – directly in MusicFree.** | |
| This plugin brings the entire JioSaavn catalog to MusicFree, with full search, playback, and import support. No official API needed – it works by interacting directly with JioSaavn's public web endpoints. | |
| --- | |
| ## ✨ Features | |
| - **Search** – Search for songs, albums, playlists, and artists using the tabs in the search bar. | |
| - **Playback** – Decrypted audio streams with quality selection (low, standard, high, super). | |
| - **Album details** – View full tracklists for any album. | |
| - **Playlist details** – Browse tracks inside playlists (with pagination). | |
| - **Artist works** – View an artist's top songs and albums. | |
| - **Lyrics** – Fetch synced lyrics for supported songs (removes \`<br>\` tags for clean display). | |
| - **URL import** – Paste any JioSaavn song, album, or playlist link directly into MusicFree's Import functionality in Plugin Management to import it instantly. | |
| - **Quality selection** – Choose between 48, 96, 160, and 320 kbps (depending on availability). | |
| - **Caching** – Decrypted URLs are cached per song for fast quality switching. | |
| - **Charts** – Browse curated categories like "Trending Now", "Top Charts", "New Releases", and more. All categories are dynamic and pulled from JioSaavn's LaunchData. | |
| - **Recommended Playlists** – Explore albums and playlists through dynamically generated tags (e.g., "Trending Now", "Charts") in the Recommendations section. Pagination is supported where available. | |
| - **Configurable logging** – Set \`logLevel\` in plugin settings to \`none\`, \`error\`, \`warn\`, \`info\`, or \`debug\` to control console output. | |
| --- | |
| ## 🛠️ How to Use | |
| 1. **Search** – Type a song, album, playlist, or artist name into MusicFree's search bar. Use the tabs to filter between \`Single\`, \`Album\`, \`Artist\`, or \`Playlist\`. | |
| 2. **Import** – Paste a JioSaavn URL (e.g., \`https://www.jiosaavn.com/song/...\`) using the \`Import Song\` in Plugin Management to instantly import the content. | |
| 3. **Play** – Tap a song to start playback. The plugin automatically decrypts the audio stream. | |
| 4. **Quality** – If your MusicFree version supports it, you can change quality in the playback settings. | |
| 5. **Charts** – Open the "Charts" section in MusicFree to see curated categories and browse albums/playlists. | |
| 6. **Recommended Playlists** – Open the "Recommend Playlists" section to discover albums and playlists based on dynamic tags. | |
| --- | |
| ## 🔧 Plugin Settings | |
| | Setting | Description | | |
| |---------|-------------| | |
| | \`Log Level\` | Control how much debug output the plugin prints. Options: \`none\`, \`error\`, \`warn\`, \`info\`, \`debug\`. Default: \`error\`. | | |
| --- | |
| ## 📦 Notes | |
| - Decryption is handled locally using \`crypto-js\` – no third‑party servers are involved. | |
| - The plugin respects your privacy – no tracking or analytics. | |
| - All API calls are made directly to JioSaavn's public endpoints. | |
| - Charts and Recommendations are powered by JioSaavn's \`webapi.getLaunchData\` endpoint, providing dynamic and up‑to‑date categories. | |
| --- | |
| ## 🐛 Reporting Issues | |
| If you encounter a bug, please include: | |
| 1. The action you were performing (search, play, import, etc.). | |
| 2. The \`logLevel\` set to \`debug\` and the relevant console output. | |
| 3. The MusicFree version and your device/OS. | |
| --- | |
| ## 🙏 Credits | |
| - Built for the [MusicFree](https://github.com/maotoumao/MusicFree) ecosystem. | |
| - JioSaavn content is property of its respective owners. | |
| ### Enjoy the music! 🎵 | |
| `, | |
| srcUrl: "https://gist.githubusercontent.com/HemanthJabalpuri/79a05fbbdee5be2a1760977b5f18ea95/raw", | |
| cacheControl: "cache", | |
| search, | |
| supportedSearchType: ['music', 'album', 'sheet', 'artist'], | |
| getLyric, | |
| getAlbumInfo: (item, page) => getSheetInfo(item, page), | |
| getMusicSheetInfo: (item, page) => getSheetInfo(item, page), | |
| getArtistWorks, | |
| getMediaSource, | |
| getTopLists, | |
| getTopListDetail: (item, page) => getSheetInfo(item, page), | |
| getRecommendSheetTags, | |
| getRecommendSheetsByTag, | |
| // import songs | |
| importMusicItem, | |
| importMusicSheet, | |
| hints: { | |
| importMusicItem: [ | |
| "1. Open a song in the JioSaavn app or website.", | |
| "2. Tap Share and select 'Copy link'.", | |
| "3. Paste the link into the MusicFree search bar." | |
| ], | |
| importMusicSheet: [ | |
| "1. Open an album or playlist in the JioSaavn app or website.", | |
| "2. Tap Share and select 'Copy link'.", | |
| "3. Paste the link into the MusicFree search bar." | |
| ] | |
| }, | |
| userVariables: [ | |
| { key: "logLevel", name: "Log Level", hint: "debug/none" }, | |
| { key: "languages", name: "Languages", hint: "hindi,telugu" }, | |
| ] | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment