initial commit
This commit is contained in:
141
README.md
Normal file
141
README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Xtream Codes Channel Search
|
||||
|
||||
A fast, interactive Python tool for searching channels across Xtream Codes IPTV services. Features multithreaded searching with progress bars and a clean, user-friendly interface.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔍 **Interactive Search Mode** - Connect once, search multiple times
|
||||
- ⚡ **Multithreaded** - Fast concurrent searches across categories
|
||||
- 📊 **Progress Bars** - Real-time progress with ETA
|
||||
- 🎯 **Content Type Filtering** - Search Live, VOD, Series separately or all together
|
||||
- 🧹 **Clean Output** - Compact, readable results
|
||||
- 🔧 **Debug Mode** - API connectivity testing and troubleshooting
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or download this repository
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode (Recommended)
|
||||
```bash
|
||||
python xtream-search.py --api-url "http://your-server.com" --username "your_user" --password "your_pass"
|
||||
```
|
||||
|
||||
### One-time Search
|
||||
```bash
|
||||
python xtream-search.py --api-url "http://your-server.com" --username "your_user" --password "your_pass" --channel "cnn"
|
||||
```
|
||||
|
||||
### Advanced Options
|
||||
```bash
|
||||
# Search all content types (Live + VOD + Series)
|
||||
python xtream-search.py --api-url "http://your-server.com" --username "user" --password "pass" --channel "movie" --all-types
|
||||
|
||||
# Increase thread count for faster searching
|
||||
python xtream-search.py --api-url "http://your-server.com" --username "user" --password "pass" --max-workers 20
|
||||
|
||||
# Debug mode to test API connectivity
|
||||
python xtream-search.py --api-url "http://your-server.com" --username "user" --password "pass" --debug
|
||||
```
|
||||
|
||||
## Interactive Search Commands
|
||||
|
||||
Once in interactive mode, use these search commands:
|
||||
|
||||
- **`channel_name`** - Search live streams (default)
|
||||
- **`live:espn`** - Search only live streams
|
||||
- **`vod:movie`** - Search only VOD content
|
||||
- **`series:friends`** - Search only series
|
||||
- **`all:news`** - Search all content types
|
||||
- **`debug`** - Show API information
|
||||
- **`quit`** or **`exit`** - Exit program
|
||||
|
||||
## Sample Output
|
||||
|
||||
```
|
||||
Connected to: http://your-server.com
|
||||
Commands: live:<term> | vod:<term> | series:<term> | all:<term> | debug | quit
|
||||
--------------------------------------------------
|
||||
|
||||
🔍 Search: cnn
|
||||
|
||||
📡 Fetching live categories...
|
||||
✅ Found 25 categories to search
|
||||
🔍 Searching live categories: 100%|████████████| 25/25 [00:03<00:00]
|
||||
|
||||
✅ Found 3 result(s) for 'cnn':
|
||||
1. [Live] News Channels → CNN International
|
||||
2. [Live] US News → CNN USA
|
||||
3. [Live] Breaking News → CNN Breaking News
|
||||
|
||||
🔍 Search: vod:action
|
||||
|
||||
📡 Fetching VOD categories...
|
||||
✅ Found 15 VOD categories
|
||||
🔍 Searching vod: 100%|████████████| 15/15 [00:02<00:00]
|
||||
|
||||
✅ Found 5 result(s) for 'action':
|
||||
1. [VOD] Action Movies → Die Hard
|
||||
2. [VOD] Action Movies → Mad Max
|
||||
3. [VOD] Thriller → Action Jackson
|
||||
4. [VOD] Adventures → Action Heroes
|
||||
5. [VOD] Classics → Action Classics
|
||||
```
|
||||
|
||||
## Command Line Arguments
|
||||
|
||||
| Argument | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `--api-url` | ✅ | Xtream Codes server URL |
|
||||
| `--username` | ✅ | Your username |
|
||||
| `--password` | ✅ | Your password |
|
||||
| `--channel` | ❌ | Channel to search (one-time mode) |
|
||||
| `--all-types` | ❌ | Search Live + VOD + Series |
|
||||
| `--interactive` | ❌ | Force interactive mode |
|
||||
| `--max-workers` | ❌ | Number of threads (default: 10) |
|
||||
| `--debug` | ❌ | Show API debug information |
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
- **Default threads (10)**: Good balance for most servers
|
||||
- **High performance (15-20 threads)**: Faster for powerful servers
|
||||
- **Conservative (5 threads)**: Better for slower connections or servers with rate limits
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6+
|
||||
- requests >= 2.25.0
|
||||
- tqdm >= 4.60.0
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
```bash
|
||||
# Test API connectivity
|
||||
python xtream-search.py --api-url "http://your-server.com" --username "user" --password "pass" --debug
|
||||
```
|
||||
|
||||
### Slow Performance
|
||||
```bash
|
||||
# Reduce thread count if getting timeouts
|
||||
python xtream-search.py --api-url "http://your-server.com" --username "user" --password "pass" --max-workers 5
|
||||
```
|
||||
|
||||
### No Results Found
|
||||
- Check your search term spelling
|
||||
- Try partial matches (e.g., "cnn" instead of "CNN International")
|
||||
- Use `all:term` to search across all content types
|
||||
- Verify your credentials with `--debug`
|
||||
|
||||
## License
|
||||
|
||||
MIT License - feel free to modify and distribute.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests welcome! Please ensure code follows the existing style and includes appropriate error handling.
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests>=2.25.0
|
||||
tqdm>=4.60.0
|
||||
451
xtream-search.py
Normal file
451
xtream-search.py
Normal file
@@ -0,0 +1,451 @@
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
from urllib.parse import urljoin
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import threading
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
print("Installing tqdm for progress bars...")
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "tqdm"])
|
||||
from tqdm import tqdm
|
||||
|
||||
def search_category(api_url, username, password, channel_name, category, pbar, pbar_lock):
|
||||
"""
|
||||
Search a single category for channels (for multithreading)
|
||||
"""
|
||||
category_id = category.get('category_id')
|
||||
category_name = category.get('category_name', 'Unknown')
|
||||
|
||||
# Create a new session for this thread
|
||||
session = requests.Session()
|
||||
categories_url = urljoin(api_url.rstrip('/'), '/player_api.php')
|
||||
|
||||
matches_in_category = []
|
||||
|
||||
try:
|
||||
# Get streams for this category
|
||||
streams_params = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'action': 'get_live_streams',
|
||||
'category_id': category_id
|
||||
}
|
||||
|
||||
streams_response = session.get(categories_url, params=streams_params)
|
||||
streams_response.raise_for_status()
|
||||
streams = streams_response.json()
|
||||
|
||||
# Check if channel exists in this category (fuzzy matching)
|
||||
for stream in streams:
|
||||
stream_name = stream.get('name', '').lower()
|
||||
if channel_name.lower() in stream_name:
|
||||
matches_in_category.append({
|
||||
'group_name': category_name,
|
||||
'group_id': category_id,
|
||||
'channel_name': stream.get('name'),
|
||||
'channel_id': stream.get('stream_id'),
|
||||
'stream_type': stream.get('stream_type'),
|
||||
'stream_icon': stream.get('stream_icon'),
|
||||
'epg_channel_id': stream.get('epg_channel_id')
|
||||
})
|
||||
|
||||
if matches_in_category:
|
||||
with pbar_lock:
|
||||
pbar.write(f" ✅ Found {len(matches_in_category)} match(es) in {category_name}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
with pbar_lock:
|
||||
pbar.write(f" ❌ Error in {category_name}: {str(e)[:50]}...")
|
||||
|
||||
finally:
|
||||
with pbar_lock:
|
||||
pbar.update(1)
|
||||
|
||||
return matches_in_category
|
||||
|
||||
def find_channel_in_groups(api_url, username, password, channel_name, max_workers=10):
|
||||
"""
|
||||
Find which groups contain a specific channel using Xtream Codes API (multithreaded)
|
||||
|
||||
Args:
|
||||
api_url (str): Base URL for the Xtream Codes API
|
||||
username (str): Username for authentication
|
||||
password (str): Password for authentication
|
||||
channel_name (str): Channel name to search for (supports fuzzy matching)
|
||||
max_workers (int): Maximum number of threads to use
|
||||
|
||||
Returns:
|
||||
list: List of categories containing the channel
|
||||
"""
|
||||
session = requests.Session()
|
||||
|
||||
try:
|
||||
# Get all live categories
|
||||
print("📡 Fetching live categories...")
|
||||
categories_url = urljoin(api_url.rstrip('/'), '/player_api.php')
|
||||
categories_params = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'action': 'get_live_categories'
|
||||
}
|
||||
|
||||
categories_response = session.get(categories_url, params=categories_params)
|
||||
categories_response.raise_for_status()
|
||||
categories = categories_response.json()
|
||||
|
||||
print(f"✅ Found {len(categories)} categories to search")
|
||||
|
||||
matching_groups = []
|
||||
pbar_lock = threading.Lock()
|
||||
|
||||
# Search through categories with multithreading
|
||||
with tqdm(total=len(categories), desc="🔍 Searching live categories", unit="cat",
|
||||
bar_format="{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]") as pbar:
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
# Submit all tasks
|
||||
future_to_category = {
|
||||
executor.submit(search_category, api_url, username, password,
|
||||
channel_name, category, pbar, pbar_lock): category
|
||||
for category in categories
|
||||
}
|
||||
|
||||
# Collect results as they complete
|
||||
for future in as_completed(future_to_category):
|
||||
try:
|
||||
category_matches = future.result()
|
||||
matching_groups.extend(category_matches)
|
||||
except Exception as e:
|
||||
category = future_to_category[future]
|
||||
category_name = category.get('category_name', 'Unknown')
|
||||
with pbar_lock:
|
||||
pbar.write(f" ❌ Exception in {category_name}: {str(e)[:50]}...")
|
||||
|
||||
return matching_groups
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ Error connecting to Xtream Codes API: {e}")
|
||||
return []
|
||||
|
||||
def get_api_info(api_url, username, password):
|
||||
"""Get basic API information for debugging"""
|
||||
session = requests.Session()
|
||||
|
||||
try:
|
||||
# Test basic connectivity and get categories
|
||||
print("🔧 Testing API connectivity...")
|
||||
api_endpoint = urljoin(api_url.rstrip('/'), '/player_api.php')
|
||||
|
||||
# Test live categories
|
||||
categories_params = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'action': 'get_live_categories'
|
||||
}
|
||||
|
||||
categories_response = session.get(api_endpoint, params=categories_params)
|
||||
print(f"Live categories endpoint status: {categories_response.status_code}")
|
||||
|
||||
if categories_response.status_code == 200:
|
||||
categories = categories_response.json()
|
||||
print(f"Found {len(categories)} live categories")
|
||||
if categories:
|
||||
print("Sample category structure:")
|
||||
print(json.dumps(categories[0], indent=2))
|
||||
|
||||
# Test getting streams from first category
|
||||
if categories_response.status_code == 200 and categories:
|
||||
print("🔧 Testing streams endpoint...")
|
||||
first_category_id = categories[0].get('category_id')
|
||||
streams_params = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'action': 'get_live_streams',
|
||||
'category_id': first_category_id
|
||||
}
|
||||
|
||||
streams_response = session.get(api_endpoint, params=streams_params)
|
||||
print(f"Streams endpoint status: {streams_response.status_code}")
|
||||
|
||||
if streams_response.status_code == 200:
|
||||
streams = streams_response.json()
|
||||
print(f"Found {len(streams)} streams in first category")
|
||||
if streams:
|
||||
print("Sample stream structure:")
|
||||
print(json.dumps(streams[0], indent=2))
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error testing API: {e}")
|
||||
|
||||
def search_content_type_category(api_url, username, password, search_term, category, content_type, stream_action, pbar, pbar_lock):
|
||||
"""Search a single category within a content type (for multithreading)"""
|
||||
category_id = category.get('category_id')
|
||||
category_name = category.get('category_name', 'Unknown')
|
||||
|
||||
session = requests.Session()
|
||||
api_endpoint = urljoin(api_url.rstrip('/'), '/player_api.php')
|
||||
|
||||
matches_in_category = []
|
||||
|
||||
try:
|
||||
# Get streams for this category
|
||||
stream_params = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'action': stream_action,
|
||||
'category_id': category_id
|
||||
}
|
||||
|
||||
stream_response = session.get(api_endpoint, params=stream_params)
|
||||
if stream_response.status_code != 200:
|
||||
with pbar_lock:
|
||||
pbar.write(f" ❌ Failed to get {content_type.lower()} from {category_name}")
|
||||
return matches_in_category
|
||||
|
||||
streams = stream_response.json()
|
||||
|
||||
for stream in streams:
|
||||
stream_name = stream.get('name', '').lower()
|
||||
if search_term.lower() in stream_name:
|
||||
matches_in_category.append({
|
||||
'content_type': content_type,
|
||||
'group_name': category_name,
|
||||
'group_id': category_id,
|
||||
'channel_name': stream.get('name'),
|
||||
'channel_id': stream.get('stream_id') or stream.get('series_id'),
|
||||
'stream_type': stream.get('stream_type'),
|
||||
'stream_icon': stream.get('stream_icon'),
|
||||
'epg_channel_id': stream.get('epg_channel_id')
|
||||
})
|
||||
|
||||
if matches_in_category:
|
||||
with pbar_lock:
|
||||
pbar.write(f" ✅ Found {len(matches_in_category)} match(es) in {category_name}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
with pbar_lock:
|
||||
pbar.write(f" ❌ Error in {category_name}: {str(e)[:50]}...")
|
||||
|
||||
finally:
|
||||
with pbar_lock:
|
||||
pbar.update(1)
|
||||
|
||||
return matches_in_category
|
||||
|
||||
def search_all_content_types(api_url, username, password, search_term, max_workers=15):
|
||||
"""Search across live streams, VOD, and series (multithreaded)"""
|
||||
session = requests.Session()
|
||||
api_endpoint = urljoin(api_url.rstrip('/'), '/player_api.php')
|
||||
all_matches = []
|
||||
|
||||
content_types = [
|
||||
('Live Streams', 'get_live_categories', 'get_live_streams'),
|
||||
('VOD', 'get_vod_categories', 'get_vod_streams'),
|
||||
('Series', 'get_series_categories', 'get_series')
|
||||
]
|
||||
|
||||
for content_type, cat_action, stream_action in content_types:
|
||||
try:
|
||||
print(f"\n<EFBFBD> Fetching {content_type} categories...")
|
||||
|
||||
# Get categories
|
||||
cat_params = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'action': cat_action
|
||||
}
|
||||
|
||||
cat_response = session.get(api_endpoint, params=cat_params)
|
||||
if cat_response.status_code != 200:
|
||||
print(f"❌ Could not get {content_type} categories (status: {cat_response.status_code})")
|
||||
continue
|
||||
|
||||
categories = cat_response.json()
|
||||
print(f"✅ Found {len(categories)} {content_type.lower()} categories")
|
||||
|
||||
if not categories:
|
||||
continue
|
||||
|
||||
pbar_lock = threading.Lock()
|
||||
|
||||
# Use multithreading for this content type
|
||||
with tqdm(total=len(categories), desc=f"🔍 Searching {content_type.lower()}", unit="cat",
|
||||
bar_format="{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]") as pbar:
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
# Submit all tasks for this content type
|
||||
future_to_category = {
|
||||
executor.submit(search_content_type_category, api_url, username, password,
|
||||
search_term, category, content_type, stream_action, pbar, pbar_lock): category
|
||||
for category in categories
|
||||
}
|
||||
|
||||
# Collect results as they complete
|
||||
for future in as_completed(future_to_category):
|
||||
try:
|
||||
category_matches = future.result()
|
||||
all_matches.extend(category_matches)
|
||||
except Exception as e:
|
||||
category = future_to_category[future]
|
||||
category_name = category.get('category_name', 'Unknown')
|
||||
with pbar_lock:
|
||||
pbar.write(f" ❌ Exception in {category_name}: {str(e)[:50]}...")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error searching {content_type}: {e}")
|
||||
continue
|
||||
|
||||
return all_matches
|
||||
|
||||
def interactive_search(api_url, username, password, max_workers=10):
|
||||
"""Interactive search mode"""
|
||||
print(f"Connected to: {api_url}")
|
||||
print("Commands: live:<term> | vod:<term> | series:<term> | all:<term> | debug | quit")
|
||||
print("-" * 50)
|
||||
|
||||
while True:
|
||||
try:
|
||||
query = input("\n🔍 Search: ").strip()
|
||||
|
||||
if not query:
|
||||
continue
|
||||
|
||||
if query.lower() in ['quit', 'exit', 'q']:
|
||||
print("👋 Goodbye!")
|
||||
break
|
||||
|
||||
if query.lower() == 'debug':
|
||||
print("\n=== API Debug Info ===")
|
||||
get_api_info(api_url, username, password)
|
||||
print("=====================")
|
||||
continue
|
||||
|
||||
# Parse search type and term
|
||||
search_all_types = False
|
||||
search_term = query
|
||||
|
||||
if ':' in query:
|
||||
search_type, search_term = query.split(':', 1)
|
||||
search_type = search_type.lower().strip()
|
||||
search_term = search_term.strip()
|
||||
|
||||
if search_type == 'all':
|
||||
search_all_types = True
|
||||
elif search_type in ['vod', 'series']:
|
||||
# For now, we'll treat these as all-types searches
|
||||
# You could implement specific VOD/series-only searches later
|
||||
search_all_types = True
|
||||
print(f"🔍 Searching {search_type.upper()} for '{search_term}'...")
|
||||
elif search_type == 'live':
|
||||
print(f"🔍 Searching Live Streams for '{search_term}'...")
|
||||
else:
|
||||
print(f"❓ Unknown search type '{search_type}', searching live streams for '{query}'...")
|
||||
search_term = query
|
||||
else:
|
||||
print(f"🔍 Searching Live Streams for '{search_term}'...")
|
||||
|
||||
# Perform search
|
||||
if search_all_types:
|
||||
matching_groups = search_all_content_types(
|
||||
api_url, username, password, search_term, max_workers
|
||||
)
|
||||
else:
|
||||
matching_groups = find_channel_in_groups(
|
||||
api_url, username, password, search_term, max_workers
|
||||
)
|
||||
|
||||
# Display results
|
||||
if matching_groups:
|
||||
print(f"\n✅ Found {len(matching_groups)} result(s) for '{search_term}':")
|
||||
for i, match in enumerate(matching_groups, 1):
|
||||
content_type = match.get('content_type', 'Live Stream')
|
||||
type_short = content_type.replace(' Streams', '').replace(' ', '')
|
||||
print(f"{i:2d}. [{type_short}] {match['group_name']} → {match['channel_name']}")
|
||||
else:
|
||||
print(f"❌ No results found for '{search_term}'")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 Goodbye!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error during search: {e}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Xtream Codes Channel Search Tool')
|
||||
parser.add_argument('--api-url', required=True, help='Xtream Codes API base URL')
|
||||
parser.add_argument('--username', required=True, help='Username for authentication')
|
||||
parser.add_argument('--password', required=True, help='Password for authentication')
|
||||
|
||||
# Optional one-time search arguments
|
||||
parser.add_argument('--channel', help='Channel name to search for (one-time search)')
|
||||
parser.add_argument('--debug', action='store_true', help='Show API debugging info')
|
||||
parser.add_argument('--all-types', action='store_true', help='Search in Live, VOD, and Series (one-time search)')
|
||||
parser.add_argument('--interactive', '-i', action='store_true', help='Start interactive search mode')
|
||||
parser.add_argument('--max-workers', type=int, default=10, help='Maximum number of concurrent threads (default: 10)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Test connection first
|
||||
print("🔧 Testing connection to Xtream Codes API...")
|
||||
try:
|
||||
session = requests.Session()
|
||||
test_url = urljoin(args.api_url.rstrip('/'), '/player_api.php')
|
||||
test_params = {
|
||||
'username': args.username,
|
||||
'password': args.password,
|
||||
'action': 'get_live_categories'
|
||||
}
|
||||
response = session.get(test_url, params=test_params, timeout=10)
|
||||
if response.status_code == 200:
|
||||
print("✅ Connection successful!")
|
||||
else:
|
||||
print(f"❌ Connection failed with status {response.status_code}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"❌ Connection failed: {e}")
|
||||
return
|
||||
|
||||
if args.debug:
|
||||
print("\n=== API Debug Info ===")
|
||||
get_api_info(args.api_url, args.username, args.password)
|
||||
print("=====================\n")
|
||||
|
||||
# If channel is provided, do one-time search
|
||||
if args.channel:
|
||||
print(f"🔍 Searching for '{args.channel}' in Xtream Codes API...")
|
||||
|
||||
if args.all_types:
|
||||
matching_groups = search_all_content_types(
|
||||
args.api_url,
|
||||
args.username,
|
||||
args.password,
|
||||
args.channel,
|
||||
args.max_workers
|
||||
)
|
||||
else:
|
||||
matching_groups = find_channel_in_groups(
|
||||
args.api_url,
|
||||
args.username,
|
||||
args.password,
|
||||
args.channel,
|
||||
args.max_workers
|
||||
)
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
if matching_groups:
|
||||
print(f"✅ Found {len(matching_groups)} result(s) for '{args.channel}':")
|
||||
for i, match in enumerate(matching_groups, 1):
|
||||
content_type = match.get('content_type', 'Live Stream')
|
||||
type_short = content_type.replace(' Streams', '').replace(' ', '')
|
||||
print(f"{i:2d}. [{type_short}] {match['group_name']} → {match['channel_name']}")
|
||||
else:
|
||||
print(f"❌ No groups found containing '{args.channel}'")
|
||||
|
||||
# Start interactive mode if requested or no channel provided
|
||||
if args.interactive or not args.channel:
|
||||
interactive_search(args.api_url, args.username, args.password, args.max_workers)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user