@sn5u/flixhq-api 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 staticsenju
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # FlixHQ API
2
+
3
+ A specialized Node.js library that provides an API for obtaining movie and TV show information, including metadata, seasons, episodes, and streaming sources from FlixHQ.
4
+
5
+ ## Features
6
+
7
+ - **Home Data**: Fetch trending movies, TV shows, and latest releases.
8
+ - **Search**: Advanced search functionality for movies and series.
9
+ - **Filters**: Filter content by type (Movie/TV), Genre, Country, and Top IMDB.
10
+ - **Details**: Scrape full metadata including plot, year, country, and cast.
11
+ - **Episodes**: Retrieve full season and episode lists for TV series.
12
+ - **Streaming**: Extract server lists and streaming sources (HLS/m3u8).
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install flixhq-api
18
+
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```javascript
24
+ const FlixHQ = require('flixhq-api');
25
+
26
+ const flixhq = new FlixHQ();
27
+
28
+ (async () => {
29
+ const data = await flixhq.search("The Last of Us");
30
+ console.log(data);
31
+ })();
32
+
33
+ ```
34
+
35
+ ## API Methods
36
+
37
+ ### Content Discovery
38
+
39
+ #### `fetchHome()`
40
+
41
+ Fetches data from the homepage, including Trending Movies, Trending TV, Latest Movies, and Latest TV.
42
+
43
+ ```javascript
44
+ const home = await flixhq.fetchHome();
45
+ console.log(home.trendingMovies);
46
+
47
+ ```
48
+
49
+ #### `fetchMovies(page)`
50
+
51
+ Fetches a list of movies.
52
+
53
+ * `page` (number, optional): Default is 1.
54
+
55
+ ```javascript
56
+ const movies = await flixhq.fetchMovies(1);
57
+
58
+ ```
59
+
60
+ #### `fetchTVShows(page)`
61
+
62
+ Fetches a list of TV shows.
63
+
64
+ * `page` (number, optional): Default is 1.
65
+
66
+ ```javascript
67
+ const tvShows = await flixhq.fetchTVShows(1);
68
+
69
+ ```
70
+
71
+ #### `fetchTopIMDB(type, page)`
72
+
73
+ Fetches content sorted by IMDB rating.
74
+
75
+ * `type` (string, optional): 'all', 'movie', or 'tv'. Default 'all'.
76
+ * `page` (number, optional): Default is 1.
77
+
78
+ ```javascript
79
+ const topRated = await flixhq.fetchTopIMDB('movie', 1);
80
+
81
+ ```
82
+
83
+ ### Search & Filters
84
+
85
+ #### `search(query)`
86
+
87
+ Search for movies or TV shows by title.
88
+
89
+ ```javascript
90
+ const results = await flixhq.search("Breaking Bad");
91
+ // Returns: [{ id: 'breaking-bad-12345', title: 'Breaking Bad', ... }]
92
+
93
+ ```
94
+
95
+ #### `fetchGenres()` & `fetchCountries()`
96
+
97
+ Returns a list of available genres and countries for filtering.
98
+
99
+ ```javascript
100
+ const genres = await flixhq.fetchGenres();
101
+ const countries = await flixhq.fetchCountries();
102
+
103
+ ```
104
+
105
+ #### `filter(type, value, page)`
106
+
107
+ Filter content by specific criteria.
108
+
109
+ * `type`: The filter category (e.g., 'genre', 'country').
110
+ * `value`: The specific slug (e.g., 'action', 'us').
111
+
112
+ ```javascript
113
+ const actionMovies = await flixhq.filter('genre', 'action', 1);
114
+
115
+ ```
116
+
117
+ ### Metadata & Media Info
118
+
119
+ #### `getDetails(slug)`
120
+
121
+ Get detailed information about a specific media item.
122
+
123
+ * `slug`: The unique string ID (e.g., `movie/watch-avatar-12345`).
124
+
125
+ ```javascript
126
+ const info = await flixhq.getDetails("movie/watch-avatar-12345");
127
+ /* Returns:
128
+ {
129
+ id: "12345",
130
+ title: "Avatar",
131
+ description: "...",
132
+ year: 2009,
133
+ released: "2009-12-18",
134
+ genres: ["Action", "Sci-Fi"]
135
+ }
136
+ */
137
+
138
+ ```
139
+
140
+ #### `getSeasons(slug)`
141
+
142
+ Get available seasons for a TV show.
143
+
144
+ ```javascript
145
+ const seasons = await flixhq.getSeasons("tv/watch-stranger-things-39444");
146
+
147
+ ```
148
+
149
+ #### `getEpisodes(seasonId)`
150
+
151
+ Get all episodes for a specific season ID.
152
+
153
+ ```javascript
154
+ const episodes = await flixhq.getEpisodes("12345"); // seasonId returned from getSeasons
155
+
156
+ ```
157
+
158
+ ### Streaming
159
+
160
+ #### `getServers(contentId, type)`
161
+
162
+ Get available streaming servers for a movie or episode.
163
+
164
+ * `contentId`: The ID of the movie or the specific episode ID.
165
+ * `type`: 'movie' or 'tv'.
166
+
167
+ ```javascript
168
+ // For a movie
169
+ const servers = await flixhq.getServers("12345", "movie");
170
+
171
+ // For an episode
172
+ const servers = await flixhq.getServers("episode-id-67890", "tv");
173
+
174
+ ```
175
+
176
+ #### `fetchSource(serverId)`
177
+
178
+ Extracts the streaming link (m3u8) for a specific server ID.
179
+
180
+ ```javascript
181
+ const source = await flixhq.fetchSource("server-id-54321");
182
+ /* Returns:
183
+ {
184
+ source: "https://.../master.m3u8",
185
+ type: "hls",
186
+ encrypted: false
187
+ }
188
+ */
189
+
190
+ ```
191
+
192
+ ## Dependencies
193
+
194
+ * **Axios**: For HTTP requests.
195
+ * **Cheerio**: For DOM parsing and scraping.
196
+
197
+ ## Disclaimer
198
+
199
+ This library is for educational purposes only. The authors do not endorse or promote copyright infringement. Users are responsible for ensuring they comply with local laws and terms of service of the target websites.
200
+
201
+ ```
202
+
203
+ ```
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ const FlixHQ = require('./src/FlixHQ');
2
+
3
+ module.exports = FlixHQ;
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@sn5u/flixhq-api",
3
+ "version": "1.0.0",
4
+ "description": "A robust Node.js API for scraping movies, TV shows, and streaming sources from FlixHQ.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/staticsenju/flixhq-api.git"
12
+ },
13
+ "keywords": [
14
+ "flixhq",
15
+ "movie-api",
16
+ "scraper",
17
+ "streaming",
18
+ "crawler"
19
+ ],
20
+ "author": "staticsenju",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "axios": "^1.6.0",
24
+ "cheerio": "^1.0.0-rc.12"
25
+ }
26
+ }
package/src/FlixHQ.js ADDED
@@ -0,0 +1,257 @@
1
+ const axios = require('axios');
2
+ const cheerio = require('cheerio');
3
+ const { URL } = require('url');
4
+
5
+ class FlixHQ {
6
+ constructor() {
7
+ this.baseUrl = 'https://flixhq.to';
8
+ this.headers = {
9
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
10
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
11
+ 'X-Requested-With': 'XMLHttpRequest'
12
+ };
13
+ }
14
+
15
+ _parseGrid($) {
16
+ const items = [];
17
+ $('.flw-item').each((_, element) => {
18
+ const $el = $(element);
19
+ const $link = $el.find('.film-name a');
20
+ const title = $link.text().trim();
21
+ const href = $link.attr('href') || '';
22
+ const slug = href.replace(/^\//, '');
23
+ const poster = $el.find('img').attr('data-src') || $el.find('img').attr('src');
24
+
25
+ const yearRaw = $el.find('.fdi-item').first().text().trim();
26
+ const year = parseInt(yearRaw) || null;
27
+
28
+ const duration = $el.find('.fdi-duration').text().trim() || $el.find('.fdi-item').eq(2).text().trim();
29
+ const quality = $el.find('.film-poster-quality').text().trim();
30
+
31
+ let type = 'movie';
32
+ if (href.includes('/tv/') || $el.find('.fdi-type').text().trim() === 'TV') {
33
+ type = 'tv';
34
+ }
35
+
36
+ if (title && slug) {
37
+ items.push({
38
+ id: slug.split('-').pop(),
39
+ title, slug, poster, year, duration, quality, type
40
+ });
41
+ }
42
+ });
43
+ return items;
44
+ }
45
+
46
+ async fetchHome() {
47
+ try {
48
+ const { data } = await axios.get(`${this.baseUrl}/home`, { headers: this.headers });
49
+ const $ = cheerio.load(data);
50
+ const trendingMovies = [];
51
+ $('#trending-movies .flw-item').each((_, el) => trendingMovies.push(this._parseSingleItem($(el), 'movie')));
52
+ const trendingTV = [];
53
+ $('#trending-tv .flw-item').each((_, el) => trendingTV.push(this._parseSingleItem($(el), 'tv')));
54
+ const latestMovies = [];
55
+ const latestTV = [];
56
+ $('.block_area').each((_, block) => {
57
+ const heading = $(block).find('.cat-heading').text().trim();
58
+ if (heading === 'Latest Movies') $(block).find('.flw-item').each((_, el) => latestMovies.push(this._parseSingleItem($(el), 'movie')));
59
+ else if (heading === 'Latest TV Shows') $(block).find('.flw-item').each((_, el) => latestTV.push(this._parseSingleItem($(el), 'tv')));
60
+ });
61
+ return { trendingMovies, trendingTV, latestMovies, latestTV };
62
+ } catch (e) { return {}; }
63
+ }
64
+
65
+ _parseSingleItem($el, forcedType) {
66
+ const title = $el.find('.film-name a').text().trim();
67
+ const href = $el.find('.film-name a').attr('href') || '';
68
+ const slug = href.replace(/^\//, '');
69
+ const poster = $el.find('img').attr('data-src') || $el.find('img').attr('src');
70
+ const quality = $el.find('.film-poster-quality').text().trim();
71
+ const year = parseInt($el.find('.fdi-item').first().text().trim()) || null;
72
+ return {
73
+ id: slug.split('-').pop(),
74
+ title, slug, poster, year, quality,
75
+ type: forcedType || (href.includes('/tv/') ? 'tv' : 'movie')
76
+ };
77
+ }
78
+
79
+ async fetchMovies(page = 1) {
80
+ try {
81
+ const { data } = await axios.get(`${this.baseUrl}/movie?page=${page}`, { headers: this.headers });
82
+ return this._parseGrid(cheerio.load(data));
83
+ } catch (e) { return []; }
84
+ }
85
+
86
+ async fetchTVShows(page = 1) {
87
+ try {
88
+ const { data } = await axios.get(`${this.baseUrl}/tv-show?page=${page}`, { headers: this.headers });
89
+ return this._parseGrid(cheerio.load(data));
90
+ } catch (e) { return []; }
91
+ }
92
+
93
+ async fetchTopIMDB(type = 'all', page = 1) {
94
+ try {
95
+ const { data } = await axios.get(`${this.baseUrl}/top-imdb?type=${type}&page=${page}`, { headers: this.headers });
96
+ return this._parseGrid(cheerio.load(data));
97
+ } catch (e) { return []; }
98
+ }
99
+
100
+ async fetchFilters() {
101
+ try {
102
+ const { data } = await axios.get(`${this.baseUrl}/movie`, { headers: this.headers });
103
+ const $ = cheerio.load(data);
104
+ const genres = [];
105
+ $('.genre-ids').each((_, el) => genres.push({ id: $(el).val(), name: $(el).next('label').text().trim() }));
106
+ const countries = [];
107
+ $('.country-ids').each((_, el) => countries.push({ id: $(el).val(), name: $(el).next('label').text().trim() }));
108
+ const years = [];
109
+ $('input[name="release_year"]').each((_, el) => { if ($(el).val() !== 'all') years.push($(el).val()); });
110
+ return { genres, countries, years, qualities: ['HD', 'SD', 'CAM'] };
111
+ } catch (e) { return {}; }
112
+ }
113
+
114
+ async fetchGenres() { const f = await this.fetchFilters(); return f.genres || []; }
115
+ async fetchCountries() { const f = await this.fetchFilters(); return f.countries || []; }
116
+
117
+ async filter(type, value, page = 1) {
118
+ try {
119
+ const { data } = await axios.get(`${this.baseUrl}/${type}/${value}?page=${page}`, { headers: this.headers });
120
+ return this._parseGrid(cheerio.load(data));
121
+ } catch (e) { return []; }
122
+ }
123
+
124
+ async search(query) {
125
+ try {
126
+ const formatted = query.trim().replace(/[\s\W]+/g, '-');
127
+ const { data } = await axios.get(`${this.baseUrl}/search/${formatted}`, { headers: this.headers });
128
+ return this._parseGrid(cheerio.load(data));
129
+ } catch (error) { return []; }
130
+ }
131
+
132
+ async getDetails(slug) {
133
+ try {
134
+ const { data } = await axios.get(`${this.baseUrl}/${slug}`, { headers: this.headers });
135
+ const $ = cheerio.load(data);
136
+ const getRow = (label) => $('.row-line').filter((_, e) => $(e).find('.type').text().trim() === label).text().replace(label, '').trim();
137
+ const released = getRow('Released:');
138
+ return {
139
+ id: slug.split('-').pop(),
140
+ title: $('.heading-name a').text().trim(),
141
+ year: released ? parseInt(released.substring(0, 4)) : null,
142
+ description: $('.description').text().trim() || '',
143
+ country: getRow('Country:'),
144
+ released,
145
+ genres: $('.row-line').filter((_, e) => $(e).find('.type').text().includes('Genre')).find('a').map((_, el) => $(el).text().trim()).get(),
146
+ poster: $('.film-poster img').attr('src')
147
+ };
148
+ } catch (error) { return null; }
149
+ }
150
+
151
+ async getSeasons(slug) {
152
+ try {
153
+ const id = slug.split('-').pop();
154
+ const { data } = await axios.get(`${this.baseUrl}/ajax/season/list/${id}`, { headers: this.headers });
155
+ const $ = cheerio.load(data);
156
+ return $('.dropdown-item.ss-item').map((_, el) => ({
157
+ season_number: parseInt($(el).text().replace('Season ', ''), 10) || 0,
158
+ season_id: $(el).attr('data-id'),
159
+ name: $(el).text().trim()
160
+ })).get();
161
+ } catch (error) { return []; }
162
+ }
163
+
164
+ async getEpisodes(seasonId) {
165
+ try {
166
+ const { data } = await axios.get(`${this.baseUrl}/ajax/season/episodes/${seasonId}`, { headers: this.headers });
167
+ const $ = cheerio.load(data);
168
+ return $('.nav-item .eps-item').map((_, el) => {
169
+ const titleRaw = $(el).attr('title');
170
+ let number = 0, title = titleRaw;
171
+ if (titleRaw) {
172
+ const parts = titleRaw.split(':');
173
+ if (parts.length >= 2) {
174
+ number = parseInt(parts[0].replace('Eps', '').trim(), 10) || 0;
175
+ title = parts.slice(1).join(':').trim();
176
+ }
177
+ }
178
+ return { id: $(el).attr('data-id'), number, title };
179
+ }).get();
180
+ } catch (error) { return []; }
181
+ }
182
+
183
+ async getServers(contentId, type = 'tv') {
184
+ try {
185
+ const url = type === 'movie' ? `${this.baseUrl}/ajax/episode/list/${contentId}` : `${this.baseUrl}/ajax/episode/servers/${contentId}`;
186
+ const { data } = await axios.get(url, { headers: this.headers });
187
+ const $ = cheerio.load(data);
188
+ const servers = [];
189
+ $('.nav-item a').each((_, el) => {
190
+ const $el = $(el);
191
+ const id = $el.attr('data-id') || $el.attr('data-linkid');
192
+ const name = $el.attr('title') || $el.find('span').text().trim();
193
+ if (id) servers.push({ server_id: id, name: name.replace('Server ', '') });
194
+ });
195
+ return servers;
196
+ } catch (error) { return []; }
197
+ }
198
+
199
+ async extractFromEmbed(embedUrl) {
200
+ try {
201
+ const parsed = new URL(embedUrl);
202
+ const embedHost = `${parsed.protocol}//${parsed.host}`;
203
+
204
+ const { data: html } = await axios.get(embedUrl, {
205
+ headers: { 'Referer': this.baseUrl, 'User-Agent': this.headers['User-Agent'] }
206
+ });
207
+
208
+ let nonceMatch = html.match(/\b[a-zA-Z0-9]{48}\b/) ||
209
+ html.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/);
210
+
211
+ if (!nonceMatch) throw new Error("Encryption key not found in embed.");
212
+ const nonce = nonceMatch.length === 4 ? nonceMatch.slice(1).join("") : nonceMatch[0];
213
+
214
+ const fileId = embedUrl.substring(embedUrl.lastIndexOf("/") + 1).split("?")[0];
215
+ const apiUrl = `${embedHost}/embed-1/v3/e-1/getSources?id=${fileId}&_k=${nonce}`;
216
+
217
+ const { data: apiRes } = await axios.get(apiUrl, {
218
+ headers: {
219
+ 'Referer': embedHost + '/',
220
+ 'X-Requested-With': 'XMLHttpRequest',
221
+ 'User-Agent': this.headers['User-Agent']
222
+ }
223
+ });
224
+
225
+ if (!apiRes.sources || !apiRes.sources.length) throw new Error("No sources returned from provider.");
226
+
227
+ return {
228
+ source: apiRes.sources[0].file,
229
+ type: apiRes.sources[0].type || 'hls',
230
+ tracks: apiRes.tracks || [],
231
+ encrypted: apiRes.encrypted || false
232
+ };
233
+
234
+ } catch (error) {
235
+ console.error(`[Extractor] Error: ${error.message}`);
236
+ return null;
237
+ }
238
+ }
239
+
240
+ async fetchSource(serverId) {
241
+ try {
242
+ const url = `${this.baseUrl}/ajax/episode/sources/${serverId}`;
243
+ const { data } = await axios.get(url, { headers: this.headers });
244
+
245
+ const link = data.link;
246
+ if (!link) throw new Error("No embed link found for this server.");
247
+
248
+ return await this.extractFromEmbed(link);
249
+
250
+ } catch (error) {
251
+ console.error(`[FlixHQ] Source Fetch Error: ${error.message}`);
252
+ return null;
253
+ }
254
+ }
255
+ }
256
+
257
+ module.exports = FlixHQ;