iq7se2 commited on
Commit
698b416
·
verified ·
1 Parent(s): 7139bc7

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +6 -0
  2. app.py +87 -0
  3. requirements.txt +3 -0
  4. templates/index.html +293 -0
Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import aiohttp
3
+ import asyncio
4
+ from flask import Flask, render_template, request, jsonify
5
+
6
+ app = Flask(__name__)
7
+
8
+ # جلب الإعدادات الحساسة من الـ Secrets حصراً
9
+ BASE_URL = os.getenv("BASE_URL")
10
+ MANGA_PATH = os.getenv("MANGA_API_PATH")
11
+ CHAPTER_PATH = os.getenv("CHAPTER_API_PATH")
12
+
13
+ if not all([BASE_URL, MANGA_PATH, CHAPTER_PATH]):
14
+ print("⚠️ WARNING: Secrets are not fully set! The app might not work.")
15
+
16
+ PORT = int(os.getenv("PORT", 7860))
17
+
18
+ HEADERS = {
19
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
20
+ }
21
+
22
+ # ── 1. SCRAPER LOGIC ──────────────────────────────────────────────────
23
+ async def get_chapters(manga_url):
24
+ slug = manga_url.strip().rstrip("/").split("/")[-1]
25
+ # بناء الرابط باستخدام القالب المخفي
26
+ api_url = f"{BASE_URL}{MANGA_PATH.format(slug=slug)}"
27
+
28
+ async with aiohttp.ClientSession(headers=HEADERS) as session:
29
+ try:
30
+ async with session.get(api_url, timeout=15) as resp:
31
+ if resp.status != 200:
32
+ return None
33
+ data = await resp.json()
34
+ mangas = data.get("mangas", [])
35
+ if not mangas:
36
+ return None
37
+ m = mangas[0]
38
+ return {
39
+ "title": m.get("post_title", slug),
40
+ "thumbnail": m.get("thumbnail", ""),
41
+ "chapters": [
42
+ {"id": c["id_capitulo"], "title": c["nombre"], "slug": c["slug"]}
43
+ for c in m.get("capitulos", [])
44
+ ]
45
+ }
46
+ except Exception:
47
+ return None
48
+
49
+ async def get_images(chapter_id):
50
+ # بناء الرابط باستخدام القالب المخفي
51
+ api_url = f"{BASE_URL}{CHAPTER_PATH.format(id=chapter_id)}"
52
+
53
+ async with aiohttp.ClientSession(headers=HEADERS) as session:
54
+ try:
55
+ async with session.get(api_url, timeout=15) as resp:
56
+ if resp.status != 200:
57
+ return []
58
+ data = await resp.json()
59
+ imgs = data.get("imagenes") or []
60
+ return [i["src"] if isinstance(i, dict) else i for i in imgs]
61
+ except Exception:
62
+ return []
63
+
64
+ # ── 2. ROUTES ─────────────────────────────────────────────────────────
65
+ @app.route("/")
66
+ def index():
67
+ return render_template("index.html")
68
+
69
+ @app.route("/api/manga")
70
+ def api_manga():
71
+ url = request.args.get("url")
72
+ if not url: return jsonify({"error": "Missing URL"}), 400
73
+ loop = asyncio.new_event_loop()
74
+ asyncio.set_event_loop(loop)
75
+ data = loop.run_until_complete(get_chapters(url))
76
+ if not data: return jsonify({"error": "Manga not found"}), 404
77
+ return jsonify(data)
78
+
79
+ @app.route("/api/chapter/<int:chapter_id>")
80
+ def api_chapter(chapter_id):
81
+ loop = asyncio.new_event_loop()
82
+ asyncio.set_event_loop(loop)
83
+ images = loop.run_until_complete(get_images(chapter_id))
84
+ return jsonify({"images": images})
85
+
86
+ if __name__ == "__main__":
87
+ app.run(host="0.0.0.0", port=PORT)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Flask
2
+ aiohttp
3
+ beautifulsoup4
templates/index.html ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ar" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Utoon Web Reader</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Noto+Kufi+Arabic:wght@400;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #0f172a;
11
+ --card: rgba(30, 41, 59, 0.7);
12
+ --accent: #8b5cf6;
13
+ --text: #f8fafc;
14
+ --text-dim: #94a3b8;
15
+ --glass: rgba(255, 255, 255, 0.05);
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ margin: 0;
21
+ padding: 0;
22
+ font-family: 'Outfit', 'Noto Kufi Arabic', sans-serif;
23
+ }
24
+
25
+ body {
26
+ background: var(--bg);
27
+ color: var(--text);
28
+ min-height: 100vh;
29
+ overflow-x: hidden;
30
+ background-image: radial-gradient(circle at 50% -20%, #312e81, transparent);
31
+ }
32
+
33
+ .container {
34
+ max-width: 1000px;
35
+ margin: 0 auto;
36
+ padding: 2rem;
37
+ }
38
+
39
+ header {
40
+ text-align: center;
41
+ margin-bottom: 3rem;
42
+ animation: fadeInDown 0.8s ease-out;
43
+ }
44
+
45
+ h1 {
46
+ font-size: 3rem;
47
+ font-weight: 800;
48
+ background: linear-gradient(to right, #a78bfa, #f472b6);
49
+ -webkit-background-clip: text;
50
+ -webkit-text-fill-color: transparent;
51
+ margin-bottom: 0.5rem;
52
+ }
53
+
54
+ .search-box {
55
+ background: var(--card);
56
+ padding: 1.5rem;
57
+ border-radius: 24px;
58
+ backdrop-filter: blur(12px);
59
+ border: 1px solid var(--glass);
60
+ display: flex;
61
+ gap: 1rem;
62
+ box-shadow: 0 20px 50px rgba(0,0,0,0.3);
63
+ margin-bottom: 2rem;
64
+ }
65
+
66
+ input {
67
+ flex: 1;
68
+ background: rgba(15, 23, 42, 0.5);
69
+ border: 1px solid var(--glass);
70
+ padding: 1rem 1.5rem;
71
+ border-radius: 16px;
72
+ color: white;
73
+ font-size: 1rem;
74
+ outline: none;
75
+ transition: all 0.3s;
76
+ }
77
+
78
+ input:focus {
79
+ border-color: var(--accent);
80
+ box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.2);
81
+ }
82
+
83
+ button {
84
+ background: var(--accent);
85
+ color: white;
86
+ border: none;
87
+ padding: 0 2rem;
88
+ border-radius: 16px;
89
+ font-weight: 600;
90
+ cursor: pointer;
91
+ transition: transform 0.2s, background 0.2s;
92
+ }
93
+
94
+ button:hover {
95
+ background: #7c3aed;
96
+ transform: translateY(-2px);
97
+ }
98
+
99
+ .manga-info {
100
+ display: none;
101
+ animation: fadeInUp 0.5s ease-out;
102
+ }
103
+
104
+ .manga-header {
105
+ display: flex;
106
+ gap: 2rem;
107
+ margin-bottom: 2rem;
108
+ background: var(--card);
109
+ padding: 2rem;
110
+ border-radius: 24px;
111
+ border: 1px solid var(--glass);
112
+ }
113
+
114
+ .manga-header img {
115
+ width: 200px;
116
+ border-radius: 16px;
117
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
118
+ }
119
+
120
+ .chapters-grid {
121
+ display: grid;
122
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
123
+ gap: 1rem;
124
+ }
125
+
126
+ .chapter-card {
127
+ background: var(--glass);
128
+ padding: 1rem;
129
+ border-radius: 12px;
130
+ cursor: pointer;
131
+ text-align: center;
132
+ transition: all 0.2s;
133
+ border: 1px solid transparent;
134
+ }
135
+
136
+ .chapter-card:hover {
137
+ background: rgba(139, 92, 246, 0.1);
138
+ border-color: var(--accent);
139
+ transform: scale(1.03);
140
+ }
141
+
142
+ #reader {
143
+ display: none;
144
+ position: fixed;
145
+ top: 0;
146
+ left: 0;
147
+ width: 100%;
148
+ height: 100%;
149
+ background: #000;
150
+ z-index: 1000;
151
+ overflow-y: auto;
152
+ padding: 2rem 0;
153
+ }
154
+
155
+ #reader img {
156
+ display: block;
157
+ max-width: 800px;
158
+ width: 100%;
159
+ margin: 0 auto 0.5rem;
160
+ }
161
+
162
+ .close-reader {
163
+ position: fixed;
164
+ top: 1rem;
165
+ right: 1rem;
166
+ background: rgba(255,255,255,0.2);
167
+ color: white;
168
+ border: none;
169
+ width: 50px;
170
+ height: 50px;
171
+ border-radius: 50%;
172
+ font-size: 1.5rem;
173
+ cursor: pointer;
174
+ z-index: 1001;
175
+ backdrop-filter: blur(5px);
176
+ }
177
+
178
+ @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
179
+ @keyframes fadeInDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
180
+
181
+ @media (max-width: 600px) {
182
+ .manga-header { flex-direction: column; align-items: center; text-align: center; }
183
+ .search-box { flex-direction: column; }
184
+ h1 { font-size: 2rem; }
185
+ }
186
+ </style>
187
+ </head>
188
+ <body>
189
+ <div class="container">
190
+ <header>
191
+ <h1>Utoon Web</h1>
192
+ <p style="color: var(--text-dim)">استعرض فصول المانجا المفضلة لديك مباشرة</p>
193
+ </header>
194
+
195
+ <div class="search-box">
196
+ <input type="text" id="mangaUrl" placeholder="أدخل رابط المانجا هنا (مثلاً https://utoon.net/manga/eleceed/)...">
197
+ <button onclick="searchManga()">بحث</button>
198
+ </div>
199
+
200
+ <div id="loader" style="display: none; text-align: center; margin: 2rem;">
201
+ <div style="border: 4px solid var(--glass); border-top: 4px solid var(--accent); width: 40px; height: 40px; border-radius: 50%; animation: spin 1s linear infinite; margin: auto;"></div>
202
+ </div>
203
+
204
+ <div id="mangaInfo" class="manga-info">
205
+ <div class="manga-header">
206
+ <img id="mangaThumb" src="" alt="">
207
+ <div>
208
+ <h2 id="mangaTitle" style="font-size: 2rem; margin-bottom: 1rem;"></h2>
209
+ <p id="chaptersCount" style="color: var(--text-dim)"></p>
210
+ </div>
211
+ </div>
212
+ <div class="chapters-grid" id="chaptersGrid"></div>
213
+ </div>
214
+ </div>
215
+
216
+ <div id="reader">
217
+ <button class="close-reader" onclick="closeReader()">×</button>
218
+ <div id="imageStack"></div>
219
+ </div>
220
+
221
+ <script>
222
+ async function searchManga() {
223
+ const url = document.getElementById('mangaUrl').value;
224
+ if (!url) return;
225
+
226
+ document.getElementById('loader').style.display = 'block';
227
+ document.getElementById('mangaInfo').style.display = 'none';
228
+
229
+ try {
230
+ const response = await fetch(`/api/manga?url=${encodeURIComponent(url)}`);
231
+ const data = await response.json();
232
+
233
+ if (data.error) {
234
+ alert('خطأ: ' + data.error);
235
+ return;
236
+ }
237
+
238
+ document.getElementById('mangaTitle').innerText = data.title;
239
+ document.getElementById('mangaThumb').src = data.thumbnail;
240
+ document.getElementById('chaptersCount').innerText = `${data.chapters.length} فصل متوفر`;
241
+
242
+ const grid = document.getElementById('chaptersGrid');
243
+ grid.innerHTML = '';
244
+ data.chapters.forEach(ch => {
245
+ const div = document.createElement('div');
246
+ div.className = 'chapter-card';
247
+ div.innerText = ch.title;
248
+ div.onclick = () => openChapter(ch.id);
249
+ grid.appendChild(div);
250
+ });
251
+
252
+ document.getElementById('mangaInfo').style.display = 'block';
253
+ } catch (e) {
254
+ alert('فشل الاتصال بالسيرفر');
255
+ } finally {
256
+ document.getElementById('loader').style.display = 'none';
257
+ }
258
+ }
259
+
260
+ async function openChapter(id) {
261
+ document.getElementById('loader').style.display = 'block';
262
+ try {
263
+ const resp = await fetch(`/api/chapter/${id}`);
264
+ const data = await resp.json();
265
+
266
+ const stack = document.getElementById('imageStack');
267
+ stack.innerHTML = '';
268
+ data.images.forEach(src => {
269
+ const img = document.createElement('img');
270
+ img.src = src;
271
+ img.loading = 'lazy';
272
+ stack.appendChild(img);
273
+ });
274
+
275
+ document.getElementById('reader').style.display = 'block';
276
+ document.body.style.overflow = 'hidden';
277
+ } catch (e) {
278
+ alert('فشل تحميل الفصل');
279
+ } finally {
280
+ document.getElementById('loader').style.display = 'none';
281
+ }
282
+ }
283
+
284
+ function closeReader() {
285
+ document.getElementById('reader').style.display = 'none';
286
+ document.body.style.overflow = 'auto';
287
+ }
288
+
289
+ window.onkeyup = (e) => { if(e.key === "Escape") closeReader(); }
290
+ </script>
291
+ <style> @keyframes spin { 100% { transform: rotate(360deg); } } </style>
292
+ </body>
293
+ </html>