theariatv.github.io/index.html
2025-08-15 16:31:17 +02:00

371 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aria | Curated IPTV Channels</title>
<style>
:root {
--bg-color: #f8f9fa;
--text-color: #212529;
--muted-text-color: #6c757d;
--border-color: #dee2e6;
--accent-color: #0d6efd;
--header-bg: #ffffff;
--danger-color: #dc3545;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #121212;
--text-color: #e9ecef;
--muted-text-color: #adb5bd;
--border-color: #343a40;
--accent-color: #4dabf7;
--header-bg: #1e1e1e;
--danger-color: #f06571;
}
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.2s, color 0.2s;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 3rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 1.5rem;
}
header h1 {
font-size: 3rem;
margin: 0;
}
.project-info {
background-color: var(--header-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.project-info h2, .project-info h3 {
margin-top: 0;
}
.project-info a {
color: var(--accent-color);
text-decoration: none;
font-weight: 500;
}
.project-info a:hover {
text-decoration: underline;
}
.playlist-links p {
margin: 0.5rem 0;
}
.search-container {
margin-bottom: 1.5rem;
}
#search-input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
border-radius: 8px;
border: 1px solid var(--border-color);
background-color: var(--header-bg);
color: var(--text-color);
box-sizing: border-box;
}
.category-folder {
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 1rem;
background-color: var(--header-bg);
}
.category-header {
padding: 1rem 1.5rem;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.2rem;
font-weight: 500;
}
.channel-table {
width: 100%;
border-collapse: collapse;
}
.channel-table th, .channel-table td {
padding: 0.75rem 1.5rem;
border-top: 1px solid var(--border-color);
text-align: left;
vertical-align: middle;
}
.channel-table th {
font-weight: 600;
font-size: 0.9rem;
color: var(--muted-text-color);
}
.channel-name {
display: flex;
align-items: center;
}
.channel-table a {
color: var(--accent-color);
text-decoration: none;
font-weight: 500;
}
.unstable-icon {
color: #ffc107;
font-weight: bold;
margin-left: 8px;
cursor: help;
font-size: 1.2rem;
}
.report-button {
font-size: 0.8rem;
padding: 4px 8px;
border-radius: 5px;
border: 1px solid var(--danger-color);
background: transparent;
color: var(--danger-color);
cursor: pointer;
text-decoration: none;
display: inline-block;
}
</style>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const parseM3U = (m3uContent, isStable) => {
const lines = m3uContent.split('\n');
const channels = {};
const regex = /group-title="([^"]+)"/;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('#EXTINF:-1')) {
try {
const nextLine = lines[i + 1];
if (!nextLine || nextLine.startsWith('#')) continue;
const countryMatch = line.match(regex);
if (!countryMatch) continue;
const country = countryMatch[1].trim();
const channelName = line.split(',').pop().trim();
const streamUrl = nextLine.trim();
if (!channels[country]) channels[country] = {};
if (!channels[country][channelName]) {
channels[country][channelName] = { name: channelName, stable: isStable, url: streamUrl };
}
} catch (e) { console.error("Error parsing line:", line, e); }
}
}
return channels;
};
const Category = ({ country, channels, forceOpen }) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
setIsOpen(forceOpen);
}, [forceOpen]);
const createReportURL = (channelName) => {
const repoUrl = "https://github.com/theariatv/theariatv.github.io/issues/new";
const title = `Broken Stream: ${channelName}`;
const body = `**Channel Name:** ${channelName}\n\n**Problem:** (Please describe the issue, e.g., 'Stream does not load', 'Shows a black screen', 'Wrong content', etc.)`;
return `${repoUrl}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
};
return (
<div className="category-folder">
<div className="category-header" onClick={() => setIsOpen(!isOpen)}>
<span>{country}</span>
<span>{isOpen ? '' : '+'}</span>
</div>
{isOpen && (
<table className="channel-table">
<thead>
<tr>
<th>Channel</th>
<th>Stream Link</th>
<th>Report</th>
</tr>
</thead>
<tbody>
{channels.map(channel => (
<tr key={channel.name}>
<td>
<div className="channel-name">
{channel.name}
{!channel.stable &&
<span className="unstable-icon" title="Potentially unstable stream from aria+.m3u">!</span>
}
</div>
</td>
<td>
<a href={channel.url} target="_blank" rel="noopener noreferrer">Link</a>
</td>
<td>
<a href={createReportURL(channel.name)} target="_blank" rel="noopener noreferrer" className="report-button">Report</a>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
const App = () => {
const [allChannelData, setAllChannelData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAndParseData = async () => {
try {
const [ariaRes, ariaPlusRes] = await Promise.all([ fetch('aria.m3u'), fetch('aria+.m3u') ]);
const [ariaText, ariaPlusText] = await Promise.all([ ariaRes.text(), ariaPlusRes.text() ]);
const stableChannels = parseM3U(ariaText, true);
const unstableChannels = parseM3U(ariaPlusText, false);
const allChannels = { ...stableChannels };
for (const country in unstableChannels) {
if (!allChannels[country]) allChannels[country] = {};
for (const channelName in unstableChannels[country]) {
if (!allChannels[country][channelName]) {
allChannels[country][channelName] = unstableChannels[country][channelName];
}
}
}
const formattedData = Object.keys(allChannels).sort().map(country => ({
country,
channels: Object.values(allChannels[country]).sort((a, b) => a.name.localeCompare(b.name))
}));
setAllChannelData(formattedData);
setFilteredData(formattedData);
} catch (error) { console.error("Could not load channel data:", error); }
finally { setLoading(false); }
};
fetchAndParseData();
}, []);
useEffect(() => {
if (searchTerm === '') {
setFilteredData(allChannelData);
return;
}
const lowercasedFilter = searchTerm.toLowerCase();
const filtered = allChannelData
.map(category => ({
...category,
channels: category.channels.filter(channel => channel.name.toLowerCase().includes(lowercasedFilter))
}))
.filter(category => category.channels.length > 0);
setFilteredData(filtered);
}, [searchTerm, allChannelData]);
return (
<div className="container">
<header>
<h1>Aria</h1>
<p>A curated collection of IPTV channels from around the world.</p>
</header>
<main>
<div className="project-info">
<h2>About Aria</h2>
<p>
Aria provides a curated collection of IPTV channels from around the world. The channels are organized by their countries. Unlike our predecessor Mystique, this git is only using official streams, tvheadends, astra control panel among others.
</p>
<div className="playlist-links">
<h3>Playlist Files</h3>
<p><a href="./aria.m3u" download>Download aria.m3u</a> (Stable Streams)</p>
<p><a href="./aria+.m3u" download>Download aria+.m3u</a> (Potentially Unstable Streams)</p>
</div>
<h3>Want to help us?</h3>
<p>
You can go to the <a href="https://github.com/theariatv/theariatv.github.io/issues" target="_blank" rel="noopener noreferrer">Issues tab</a> on GitHub to request a channel-specific action, or use the "Report" button next to a channel.
</p>
</div>
<h2>Channel List</h2>
<div className="search-container">
<input
id="search-input"
type="search"
placeholder="Search for a channel..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
{loading ? (
<p>Loading channels...</p>
) : (
filteredData.map(category => (
<Category
key={category.country}
country={category.country}
channels={category.channels}
forceOpen={searchTerm.length > 0}
/>
))
)}
</main>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>