From 6ce27cbb3ec16b51613fc93dcb155aa89e0b5230 Mon Sep 17 00:00:00 2001 From: Abu Sayed Date: Sat, 15 Aug 2026 00:26:20 +0600 Subject: [PATCH] Add CityPlex source (v1.5.0) --- CHANGELOG.md | 3 ++ README.md | 3 +- addon.json | 4 +- index.js | 3 +- package.json | 4 +- public/index.html | 5 ++- server.js | 1 + sources/cityplex.js | 95 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 sources/cityplex.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d7b36..34f3788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v1.5.0 +- **New source**: CityPlex movies and series + ## v1.4.0 - **New sources**: RoarZone, FTPBD, CircleFTP, ICC FTP - **Web UI**: Configure sources at `http://127.0.0.1:7001` diff --git a/README.md b/README.md index c7bba4b..aadac8f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ npx streambdix - [FTPBD](https://ftpbd.net) - [CircleFTP](http://new.circleftp.net) - [ICC FTP](http://10.16.100.244) +- [CityPlex](https://cityplex.live/) --- @@ -62,4 +63,4 @@ Access your addon from anywhere using Cloudflare Tunnel. --- -**Made for BDIX users** \ No newline at end of file +**Made for BDIX users** diff --git a/addon.json b/addon.json index f6ef840..5b317b0 100644 --- a/addon.json +++ b/addon.json @@ -1,7 +1,7 @@ { "id": "com.streambdix", "name": "StreamBDIX", - "version": "1.4.0", + "version": "1.5.0", "description": "Stream from BDIX sites on Stremio", "resources": [ "stream" @@ -18,4 +18,4 @@ "adult": false, "p2p": false } -} \ No newline at end of file +} diff --git a/index.js b/index.js index c26ce88..0189d9d 100755 --- a/index.js +++ b/index.js @@ -16,7 +16,8 @@ const SOURCES = { roarzone: { name: 'RoarZone', urls: ['https://play.roarzone.info'] }, ftpbd: { name: 'FTPBD', urls: ['http://media.ftpbd.net:8096'] }, circleftp: { name: 'CircleFTP', urls: ['http://new.circleftp.net'] }, - iccftp: { name: 'ICC FTP', urls: ['http://10.16.100.244'] } + iccftp: { name: 'ICC FTP', urls: ['http://10.16.100.244'] }, + cityplex: { name: 'CityPlex', urls: ['https://cityplex.live'] } }; const dataDir = () => path.join(process.env.HOME || process.env.USERPROFILE, '.streambdix'); diff --git a/package.json b/package.json index e730a9a..c64ab4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "streambdix", - "version": "1.4.0", + "version": "1.5.0", "description": "Stream from BDIX sites on Stremio", "main": "index.js", "bin": { @@ -20,4 +20,4 @@ "axios": "^1.6.0", "stremio-addon-sdk": "^1.6.10" } -} \ No newline at end of file +} diff --git a/public/index.html b/public/index.html index f57e8dc..bc8c114 100644 --- a/public/index.html +++ b/public/index.html @@ -561,7 +561,8 @@

StreamBDIX

roarzone: { name: 'RoarZone', desc: 'Movies & Series (Emby)' }, ftpbd: { name: 'FTPBD', desc: 'Movies & Series (Emby)' }, circleftp: { name: 'CircleFTP', desc: 'Movies & Series' }, - iccftp: { name: 'ICC FTP', desc: 'Movies & Series' } + iccftp: { name: 'ICC FTP', desc: 'Movies & Series' }, + cityplex: { name: 'CityPlex', desc: 'Movies & Series' } }; let enabled = []; let forcedSources = []; @@ -866,4 +867,4 @@

StreamBDIX

- \ No newline at end of file + diff --git a/server.js b/server.js index 36583e4..7e4717f 100644 --- a/server.js +++ b/server.js @@ -12,6 +12,7 @@ const allSources = { ftpbd: require('./sources/ftpbd'), circleftp: require('./sources/circleftp'), iccftp: require('./sources/iccftp'), + cityplex: require('./sources/cityplex'), }; function getEnabledSources() { diff --git a/sources/cityplex.js b/sources/cityplex.js new file mode 100644 index 0000000..b013ac9 --- /dev/null +++ b/sources/cityplex.js @@ -0,0 +1,95 @@ +// StreamBDIX - By Corpse +const { extractQuality, titlesMatch, axios } = require('./utils'); + +const SOURCE_NAME = 'CityPlex'; +const BASE_URL = 'https://cityplex.live'; +const API_URL = `${BASE_URL}/api`; +const axiosConfig = { + timeout: 5000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } +}; + +async function search(query) { + try { + const response = await axios.get(`${API_URL}/search`, { + ...axiosConfig, + params: { search: query } + }); + return Array.isArray(response.data) ? response.data : []; + } catch { return []; } +} + +function findBestMatch(results, type, meta) { + const expectedType = type === 'movie' ? 'movie' : 'tv'; + const candidates = results.filter(result => result.type === expectedType); + const imdbId = meta.id || meta.imdb_id; + + if (imdbId) { + const exact = candidates.find(result => result.imdb_id === imdbId); + if (exact) return exact; + } + + return candidates + .filter(result => titlesMatch(result.title || result.original_title, meta.name)) + .sort((a, b) => { + if (!meta.year) return 0; + return Math.abs((a.year || meta.year) - meta.year) - Math.abs((b.year || meta.year) - meta.year); + })[0] || null; +} + +function streamUrl(type, id) { + const cityplexType = type === 'movie' ? 'movies' : 'tv_shows'; + return `${API_URL}/stream/video/stream?type=${cityplexType}&id=${id}`; +} + +async function getStreamTitle(url) { + try { + const response = await axios.head(url, { + ...axiosConfig, + maxRedirects: 0, + validateStatus: status => status >= 200 && status < 400 + }); + const location = response.headers.location || ''; + let filename = location.split('/').pop() || location; + try { filename = decodeURIComponent(filename); } catch { } + return extractQuality(filename); + } catch { return 'Unknown'; } +} + +async function makeStream(type, id) { + const url = streamUrl(type, id); + return { + name: SOURCE_NAME, + title: await getStreamTitle(url), + url + }; +} + +async function getSeriesStream(showId, season, episode) { + try { + const response = await axios.get(`${API_URL}/tv-shows/${showId}/${season}`, axiosConfig); + const episodes = Array.isArray(response.data) ? response.data : []; + const match = episodes.find(item => + Number(item.season_number) === Number(season) && + Number(item.episode_number) === Number(episode) + ); + return match ? [await makeStream('series', match.id)] : []; + } catch { return []; } +} + +module.exports = { + name: SOURCE_NAME, + types: ['movie', 'series'], + async getStreams(type, meta, season, episode) { + if (!meta || !meta.name) return []; + const results = await search(meta.name); + const match = findBestMatch(results, type, meta); + if (!match) return []; + + if (type === 'movie') return [await makeStream(type, match.id)]; + return await getSeriesStream(match.id, season, episode); + } +}; +