diff --git a/audible/audible.js b/audible/audible.js new file mode 100644 index 0000000..c822693 --- /dev/null +++ b/audible/audible.js @@ -0,0 +1,215 @@ +async function searchResults(keyword) { + const results = []; + let pageNumber = 1; + let hasMore = true; + + try { + while (hasMore) { + const headers = { + "next-action": await extractNextActionIdentifier(), + "Content-Type": "text/plain" + }; + console.log(JSON.stringify(headers)); + const postData = `[${pageNumber},"${keyword}",""]`; + const response = await fetchv2("https://mapple.site/audiobooks?search=" + encodeURIComponent(keyword), headers, "POST", postData); + const text = await response.text(); + console.log("Page " + pageNumber + " response:", text); + + const lines = text.split('\n').filter(line => line.trim()); + let foundBooks = false; + + for (const line of lines) { + try { + const data = JSON.parse(line.substring(line.indexOf('{'))); + + if (data.books && Array.isArray(data.books)) { + foundBooks = true; + for (const book of data.books) { + results.push({ + title: book.title || "Untitled", + image: book.image || "", + href: "https://mapple.site/listen/" + (book.$id || "") + "/" + encodeURIComponent(book.title || "untitled") + }); + } + hasMore = data.hasMore === true; + console.log("hasMore: " + hasMore); + } + } catch (e) { + } + } + + if (!foundBooks) { + hasMore = false; + } + + pageNumber++; + } + + return JSON.stringify(results); + } catch (err) { + console.log("Search error:", err); + return JSON.stringify([{ + title: "Error", + image: "Error", + href: "Error" + }]); + } +} + +async function extractDetails(url) { + try { + return JSON.stringify([{ + description: "", + aliases: "", + airdate: "" + }]); + } catch (err) { + return JSON.stringify([{ + description: "Error", + aliases: "Error", + airdate: "Error" + }]); + } +} + +async function extractEpisodes(url) { + const results = []; + try { + const fetchResponse = await fetchv2(url, { + "rsc": "1" + }); + const fetchText = await fetchResponse.text(); + + const audioItemsMatch = fetchText.match(/"audio_items":\[([^\]]+)\]/); + + if (audioItemsMatch && audioItemsMatch[1]) { + const audioItemsStr = audioItemsMatch[1]; + const urlMatches = audioItemsStr.match(/"(https?:\/\/[^"]+)"/g); + + if (urlMatches) { + for (let i = 0; i < urlMatches.length; i++) { + const audioUrl = urlMatches[i].replace(/"/g, ''); + results.push({ + href: audioUrl, + number: i + 1, + title: `Chapter ${i + 1}` + }); + } + } + } + + return JSON.stringify(results); + } catch (err) { + console.log("Episodes error:", err); + return JSON.stringify([{ + href: "Error", + number: "Error", + title: "Error" + }]); + } +} + +async function extractStreamUrl(url) { + try { + const headers = { + "Host": "ipaudio.club", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:146.0) Gecko/20100101 Firefox/146.0", + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Connection": "keep-alive", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "no-cors", + "Sec-Fetch-Site": "cross-site", + "Priority": "u=4", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + }; + + const streams = [{ + title: `Audio Stream 1`, + streamUrl: url, + headers: headers + }]; + + return JSON.stringify({ + streams: streams, + subtitle: "" + }); + + } catch (err) { + console.log("Stream error:", err); + return JSON.stringify({ + streams: [{ + title: "Error", + streamUrl: "", + headers: {} + }], + subtitle: "" + }); + } +} + +async function soraFetch(url, options = { headers: {}, method: 'GET', body: null, encoding: 'utf-8' }) { + try { + return await fetchv2( + url, + options.headers ?? {}, + options.method ?? 'GET', + options.body ?? null, + true, + options.encoding ?? 'utf-8' + ); + } catch(e) { + try { + return await fetch(url, options); + } catch(error) { + return null; + } + } +} + +async function extractNextActionIdentifier() { + const htmlResponse = await soraFetch(atob("aHR0cHM6Ly9tYXBwbGUuc2l0ZS9hdWRpb2Jvb2tz")); + const htmlText = await htmlResponse.text(); + + const pageMatch = htmlText.match(/]*src="([^"]*app\/audiobooks\/page-[^"]*\.js)"[^>]*><\/script>/); + + if (!pageMatch || !pageMatch[1]) { + throw new Error("error 1"); + } + + const beforePageMatch = htmlText.match(/]*src="([^"]*\.js)"[^>]*><\/script>]*src="[^"]*app\/audiobooks\/page-[^"]*\.js"/); + + if (!beforePageMatch || !beforePageMatch[1]) { + throw new Error("error 2"); + } + + let targetUrl = beforePageMatch[1]; + if (targetUrl.startsWith('/_next/')) { + targetUrl = 'https://mapple.site' + targetUrl; + } else if (!targetUrl.startsWith('http')) { + targetUrl = 'https://mapple.site/' + targetUrl; + } + + try { + const response = await soraFetch(targetUrl); + const text = await response.text(); + + let actionMatch = text.match(/createServerReference\)\("([a-f0-9]{40,})"[^"]*"getStreamUrl/); + if (!actionMatch) { + actionMatch = text.match(/createServerReference\)\("([a-f0-9]{40,})"/); + } + if (!actionMatch) { + actionMatch = text.match(/"([a-f0-9]{40,})"[^"]*"getStreamUrl/); + } + + if (actionMatch && actionMatch[1]) { + return actionMatch[1]; + } + } catch (e) { + throw new Error("error 3: " + e); + } + + throw new Error("error 4"); +}