broza commited on
Commit
789d654
·
verified ·
1 Parent(s): caa5e1e

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +147 -127
main.py CHANGED
@@ -75,8 +75,7 @@ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
75
  class DownloadRequest(BaseModel):
76
  url: str
77
  chapter_name: Optional[str] = None
78
- merge_images: Optional[bool] = False
79
- target_width: Optional[int] = 800
80
 
81
  class DownloadResponse(BaseModel):
82
  download_id: str
@@ -137,77 +136,95 @@ async def download_image(session, image_url, filepath, referer_url):
137
  logger.error(f"Error downloading {image_url}: {str(e)}")
138
  return False
139
 
140
- async def merge_images(image_dir, output_path, target_width=800):
141
- """Merge semua gambar menjadi satu gambar panjang"""
142
- try:
143
- logger.info(f"🖼️ Starting image merge with target width: {target_width}px")
144
-
145
- # Get all image files sorted by name
146
- image_files = []
147
- for file in sorted(os.listdir(image_dir)):
148
- if file.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
149
- image_files.append(os.path.join(image_dir, file))
150
-
151
- if not image_files:
152
- logger.error("No image files found for merging")
153
- return False
154
-
155
- logger.info(f"Found {len(image_files)} images to merge")
156
-
157
- # Load all images and calculate total height
158
- images = []
159
- total_height = 0
160
-
161
- for img_path in image_files:
162
- try:
163
- img = Image.open(img_path)
164
- # Convert to RGB if necessary
165
- if img.mode in ('RGBA', 'LA', 'P'):
166
- img = img.convert('RGB')
167
 
168
- # Resize to target width while maintaining aspect ratio
169
- original_width, original_height = img.size
170
- if original_width != target_width:
171
- new_height = int(original_height * target_width / original_width)
172
- img = img.resize((target_width, new_height), Image.Resampling.LANCZOS)
 
 
 
 
 
 
173
 
174
- images.append(img)
175
- total_height += img.height
176
- logger.info(f"Processed image: {os.path.basename(img_path)} - Size: {img.size}")
177
 
178
- except Exception as e:
179
- logger.error(f"Error processing image {img_path}: {e}")
180
- continue
181
-
182
- if not images:
183
- logger.error("No valid images to merge")
184
- return False
185
-
186
- logger.info(f"Creating merged image: {target_width}x{total_height}px")
187
-
188
- # Create the merged image
189
- merged_image = Image.new('RGB', (target_width, total_height), color='white')
190
-
191
- # Paste images vertically
192
- current_y = 0
193
- for i, img in enumerate(images):
194
- merged_image.paste(img, (0, current_y))
195
- current_y += img.height
196
- logger.info(f"Pasted image {i+1}/{len(images)} at position y={current_y-img.height}")
197
-
198
- # Save the merged image
199
- merged_image.save(output_path, 'JPEG', quality=95, optimize=True)
200
- logger.info(f"✅ Merged image saved: {output_path}")
201
-
202
- # Clean up
203
- for img in images:
204
- img.close()
205
-
206
- return True
207
-
208
- except Exception as e:
209
- logger.error(f"Error merging images: {e}")
210
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
  async def create_zip_archive(source_dir, zip_path):
213
  """Create zip archive from directory"""
@@ -218,7 +235,7 @@ async def create_zip_archive(source_dir, zip_path):
218
  arcname = os.path.relpath(file_path, source_dir)
219
  zipf.write(file_path, arcname)
220
 
221
- async def download_webtoon(url: str, download_id: str, chapter_name: str = None, merge_images_flag: bool = False, target_width: int = 800):
222
  """Main function to download webtoon"""
223
  try:
224
  download_status[download_id] = {"status": "starting", "progress": 0, "message": "Initializing..."}
@@ -291,7 +308,12 @@ async def download_webtoon(url: str, download_id: str, chapter_name: str = None,
291
 
292
  download_status[download_id] = {"status": "running", "progress": 50, "message": f"Downloading {len(image_urls)} images..."}
293
 
 
 
 
 
294
  # Download images
 
295
  async with aiohttp.ClientSession() as session:
296
  for i, image_url in enumerate(image_urls):
297
  try:
@@ -300,12 +322,14 @@ async def download_webtoon(url: str, download_id: str, chapter_name: str = None,
300
  ext = ext.split("?")[0] # Remove query parameters
301
 
302
  filename = f"{str(i).zfill(3)}{ext}"
303
- filepath = save_dir / filename
304
 
305
  logger.info(f"⬇️ Downloading image {filename}...")
306
  success = await download_image(session, image_url, filepath, referer)
307
 
308
- if not success:
 
 
309
  logger.error(f"❌ Failed to download image {filename}")
310
 
311
  # Update progress
@@ -319,21 +343,37 @@ async def download_webtoon(url: str, download_id: str, chapter_name: str = None,
319
  except Exception as e:
320
  logger.error(f"❌ Error downloading image {i}: {str(e)}")
321
 
322
- # Merge images if requested
323
- if merge_images_flag:
324
- download_status[download_id] = {"status": "running", "progress": 85, "message": "Merging images..."}
325
 
326
- merged_filename = f"{chapter_name}_merged.jpg"
327
- merged_path = save_dir / merged_filename
328
 
329
- success = await merge_images(str(save_dir), str(merged_path), target_width)
 
330
 
331
- if success:
332
- logger.info("✅ Images merged successfully!")
333
- download_status[download_id] = {"status": "running", "progress": 88, "message": "Images merged successfully!"}
334
- else:
335
- logger.error("Failed to merge images")
336
- download_status[download_id] = {"status": "running", "progress": 88, "message": "Warning: Failed to merge images, continuing with individual files..."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
  download_status[download_id] = {"status": "running", "progress": 90, "message": "Creating zip archive..."}
339
 
@@ -426,7 +466,7 @@ async def index():
426
  font-weight: 600;
427
  }
428
 
429
- input[type="text"], input[type="number"] {
430
  width: 100%;
431
  padding: 15px;
432
  border: 2px solid #e1e5e9;
@@ -435,7 +475,7 @@ async def index():
435
  transition: border-color 0.3s ease;
436
  }
437
 
438
- input[type="text"]:focus, input[type="number"]:focus {
439
  outline: none;
440
  border-color: #667eea;
441
  }
@@ -444,24 +484,18 @@ async def index():
444
  display: flex;
445
  align-items: center;
446
  gap: 10px;
447
- margin-bottom: 10px;
448
  }
449
 
450
  .checkbox-group input[type="checkbox"] {
451
  width: 20px;
452
  height: 20px;
453
- accent-color: #667eea;
454
  }
455
 
456
  .checkbox-group label {
457
- margin-bottom: 0;
458
- font-weight: 500;
459
- cursor: pointer;
460
- }
461
-
462
- .width-input {
463
- margin-top: 10px;
464
- display: none;
465
  }
466
 
467
  .btn {
@@ -546,13 +580,14 @@ async def index():
546
  display: none;
547
  }
548
 
549
- .info {
550
- background: #d1ecf1;
551
- color: #0c5460;
552
- padding: 10px;
553
- border-radius: 5px;
 
554
  font-size: 14px;
555
- margin-top: 10px;
556
  }
557
  </style>
558
  </head>
@@ -563,6 +598,10 @@ async def index():
563
  <p>Download webtoon images as ZIP archive - Free Only Chapter</p>
564
  </div>
565
 
 
 
 
 
566
  <form id="downloadForm">
567
  <div class="form-group">
568
  <label for="url">Webtoon URL:</label>
@@ -576,17 +615,8 @@ async def index():
576
 
577
  <div class="form-group">
578
  <div class="checkbox-group">
579
- <input type="checkbox" id="merge_images" name="merge_images">
580
- <label for="merge_images">Merge all images into one long image</label>
581
- </div>
582
-
583
- <div class="width-input" id="widthInput">
584
- <label for="target_width">Target Width (pixels):</label>
585
- <input type="number" id="target_width" name="target_width" value="800" min="300" max="2000" step="50">
586
- </div>
587
-
588
- <div class="info">
589
- <strong>Merge Option:</strong> When enabled, all images will be combined into one long vertical image with the specified width. Individual images will still be included in the ZIP file.
590
  </div>
591
  </div>
592
 
@@ -616,13 +646,6 @@ async def index():
616
  const downloadUrl = document.getElementById('downloadUrl');
617
  const errorMessage = document.getElementById('errorMessage');
618
  const downloadBtn = document.getElementById('downloadBtn');
619
- const mergeCheckbox = document.getElementById('merge_images');
620
- const widthInput = document.getElementById('widthInput');
621
-
622
- // Show/hide width input based on merge checkbox
623
- mergeCheckbox.addEventListener('change', function() {
624
- widthInput.style.display = this.checked ? 'block' : 'none';
625
- });
626
 
627
  form.addEventListener('submit', async (e) => {
628
  e.preventDefault();
@@ -630,8 +653,7 @@ async def index():
630
  const formData = new FormData(form);
631
  const url = formData.get('url');
632
  const chapterName = formData.get('chapter_name');
633
- const mergeImages = formData.get('merge_images') === 'on';
634
- const targetWidth = parseInt(formData.get('target_width')) || 800;
635
 
636
  if (!url) {
637
  showError('Please enter a valid URL');
@@ -655,8 +677,7 @@ async def index():
655
  body: JSON.stringify({
656
  url: url,
657
  chapter_name: chapterName || null,
658
- merge_images: mergeImages,
659
- target_width: targetWidth
660
  })
661
  });
662
 
@@ -735,8 +756,7 @@ async def start_download(request: DownloadRequest, background_tasks: BackgroundT
735
  request.url,
736
  download_id,
737
  request.chapter_name,
738
- request.merge_images,
739
- request.target_width
740
  )
741
 
742
  return DownloadResponse(
 
75
  class DownloadRequest(BaseModel):
76
  url: str
77
  chapter_name: Optional[str] = None
78
+ merge_images: Optional[bool] = True # New option to merge images
 
79
 
80
  class DownloadResponse(BaseModel):
81
  download_id: str
 
136
  logger.error(f"Error downloading {image_url}: {str(e)}")
137
  return False
138
 
139
+ async def merge_images_vertically(image_paths, output_dir, max_height=8000):
140
+ """
141
+ Menggabungkan gambar secara vertikal dengan batasan tinggi maksimal.
142
+ Jika tinggi gabungan melebihi max_height, akan dibuat file terpisah.
143
+ """
144
+ if not image_paths:
145
+ return []
146
+
147
+ merged_files = []
148
+ current_images = []
149
+ current_height = 0
150
+ file_index = 0
151
+
152
+ for image_path in image_paths:
153
+ try:
154
+ with Image.open(image_path) as img:
155
+ img_width, img_height = img.size
 
 
 
 
 
 
 
 
 
 
156
 
157
+ # Jika menambahkan gambar ini akan melebihi max_height, save current batch
158
+ if current_height + img_height > max_height and current_images:
159
+ # Save current batch
160
+ output_path = output_dir / f"{str(file_index).zfill(3)}.jpg"
161
+ await save_merged_image(current_images, output_path)
162
+ merged_files.append(output_path)
163
+
164
+ # Reset untuk batch baru
165
+ current_images = []
166
+ current_height = 0
167
+ file_index += 1
168
 
169
+ # Tambahkan gambar ke batch current
170
+ current_images.append(image_path)
171
+ current_height += img_height
172
 
173
+ logger.info(f"Added image {image_path.name} (height: {img_height}) to batch {file_index}")
174
+
175
+ except Exception as e:
176
+ logger.error(f"Error processing image {image_path}: {e}")
177
+ continue
178
+
179
+ # Save batch terakhir jika ada
180
+ if current_images:
181
+ output_path = output_dir / f"{str(file_index).zfill(3)}.jpg"
182
+ await save_merged_image(current_images, output_path)
183
+ merged_files.append(output_path)
184
+
185
+ logger.info(f"Created {len(merged_files)} merged image files")
186
+ return merged_files
187
+
188
+ async def save_merged_image(image_paths, output_path):
189
+ """Save multiple images as one merged vertical image"""
190
+ if not image_paths:
191
+ return
192
+
193
+ images = []
194
+ total_width = 0
195
+ total_height = 0
196
+
197
+ # Load all images and calculate dimensions
198
+ for image_path in image_paths:
199
+ try:
200
+ img = Image.open(image_path)
201
+ images.append(img)
202
+ total_width = max(total_width, img.width)
203
+ total_height += img.height
204
+ except Exception as e:
205
+ logger.error(f"Error loading image {image_path}: {e}")
206
+ continue
207
+
208
+ if not images:
209
+ return
210
+
211
+ # Create new image with combined height
212
+ merged_image = Image.new('RGB', (total_width, total_height), (255, 255, 255))
213
+
214
+ # Paste images vertically
215
+ y_offset = 0
216
+ for img in images:
217
+ # Center horizontally if image is narrower than total_width
218
+ x_offset = (total_width - img.width) // 2
219
+ merged_image.paste(img, (x_offset, y_offset))
220
+ y_offset += img.height
221
+ img.close() # Close to free memory
222
+
223
+ # Save merged image
224
+ merged_image.save(output_path, 'JPEG', quality=95, optimize=True)
225
+ merged_image.close()
226
+
227
+ logger.info(f"Saved merged image: {output_path} ({total_width}x{total_height})")
228
 
229
  async def create_zip_archive(source_dir, zip_path):
230
  """Create zip archive from directory"""
 
235
  arcname = os.path.relpath(file_path, source_dir)
236
  zipf.write(file_path, arcname)
237
 
238
+ async def download_webtoon(url: str, download_id: str, chapter_name: str = None, merge_images: bool = True):
239
  """Main function to download webtoon"""
240
  try:
241
  download_status[download_id] = {"status": "starting", "progress": 0, "message": "Initializing..."}
 
308
 
309
  download_status[download_id] = {"status": "running", "progress": 50, "message": f"Downloading {len(image_urls)} images..."}
310
 
311
+ # Create temp directory for individual images
312
+ temp_images_dir = save_dir / "temp_images"
313
+ temp_images_dir.mkdir(exist_ok=True)
314
+
315
  # Download images
316
+ downloaded_images = []
317
  async with aiohttp.ClientSession() as session:
318
  for i, image_url in enumerate(image_urls):
319
  try:
 
322
  ext = ext.split("?")[0] # Remove query parameters
323
 
324
  filename = f"{str(i).zfill(3)}{ext}"
325
+ filepath = temp_images_dir / filename
326
 
327
  logger.info(f"⬇️ Downloading image {filename}...")
328
  success = await download_image(session, image_url, filepath, referer)
329
 
330
+ if success:
331
+ downloaded_images.append(filepath)
332
+ else:
333
  logger.error(f"❌ Failed to download image {filename}")
334
 
335
  # Update progress
 
343
  except Exception as e:
344
  logger.error(f"❌ Error downloading image {i}: {str(e)}")
345
 
346
+ if merge_images and downloaded_images:
347
+ download_status[download_id] = {"status": "running", "progress": 80, "message": "Merging images..."}
348
+ logger.info("🔗 Merging images...")
349
 
350
+ # Sort images by filename to ensure correct order
351
+ downloaded_images.sort()
352
 
353
+ # Merge images with max height of 8000px
354
+ merged_files = await merge_images_vertically(downloaded_images, save_dir, max_height=8000)
355
 
356
+ # Clean up temp individual images
357
+ try:
358
+ shutil.rmtree(temp_images_dir)
359
+ except Exception as e:
360
+ logger.warning(f"Failed to cleanup temp images: {e}")
361
+
362
+ logger.info(f"✅ Created {len(merged_files)} merged image files")
363
+ else:
364
+ # If not merging, move individual images to save_dir
365
+ for img_path in downloaded_images:
366
+ try:
367
+ new_path = save_dir / img_path.name
368
+ shutil.move(str(img_path), str(new_path))
369
+ except Exception as e:
370
+ logger.error(f"Error moving image {img_path}: {e}")
371
+
372
+ # Clean up temp directory
373
+ try:
374
+ shutil.rmtree(temp_images_dir)
375
+ except Exception as e:
376
+ logger.warning(f"Failed to cleanup temp directory: {e}")
377
 
378
  download_status[download_id] = {"status": "running", "progress": 90, "message": "Creating zip archive..."}
379
 
 
466
  font-weight: 600;
467
  }
468
 
469
+ input[type="text"] {
470
  width: 100%;
471
  padding: 15px;
472
  border: 2px solid #e1e5e9;
 
475
  transition: border-color 0.3s ease;
476
  }
477
 
478
+ input[type="text"]:focus {
479
  outline: none;
480
  border-color: #667eea;
481
  }
 
484
  display: flex;
485
  align-items: center;
486
  gap: 10px;
487
+ margin-top: 10px;
488
  }
489
 
490
  .checkbox-group input[type="checkbox"] {
491
  width: 20px;
492
  height: 20px;
 
493
  }
494
 
495
  .checkbox-group label {
496
+ margin: 0;
497
+ font-weight: normal;
498
+ color: #666;
 
 
 
 
 
499
  }
500
 
501
  .btn {
 
580
  display: none;
581
  }
582
 
583
+ .info-box {
584
+ background: #e7f3ff;
585
+ border: 1px solid #b3d9ff;
586
+ border-radius: 8px;
587
+ padding: 15px;
588
+ margin-bottom: 20px;
589
  font-size: 14px;
590
+ color: #0066cc;
591
  }
592
  </style>
593
  </head>
 
598
  <p>Download webtoon images as ZIP archive - Free Only Chapter</p>
599
  </div>
600
 
601
+ <div class="info-box">
602
+ <strong>New Feature:</strong> Images will be merged vertically with max height of 8000px per file. Large chapters will be split into multiple files (000.jpg, 001.jpg, etc.)
603
+ </div>
604
+
605
  <form id="downloadForm">
606
  <div class="form-group">
607
  <label for="url">Webtoon URL:</label>
 
615
 
616
  <div class="form-group">
617
  <div class="checkbox-group">
618
+ <input type="checkbox" id="merge_images" name="merge_images" checked>
619
+ <label for="merge_images">Merge images vertically (recommended)</label>
 
 
 
 
 
 
 
 
 
620
  </div>
621
  </div>
622
 
 
646
  const downloadUrl = document.getElementById('downloadUrl');
647
  const errorMessage = document.getElementById('errorMessage');
648
  const downloadBtn = document.getElementById('downloadBtn');
 
 
 
 
 
 
 
649
 
650
  form.addEventListener('submit', async (e) => {
651
  e.preventDefault();
 
653
  const formData = new FormData(form);
654
  const url = formData.get('url');
655
  const chapterName = formData.get('chapter_name');
656
+ const mergeImages = document.getElementById('merge_images').checked;
 
657
 
658
  if (!url) {
659
  showError('Please enter a valid URL');
 
677
  body: JSON.stringify({
678
  url: url,
679
  chapter_name: chapterName || null,
680
+ merge_images: mergeImages
 
681
  })
682
  });
683
 
 
756
  request.url,
757
  download_id,
758
  request.chapter_name,
759
+ request.merge_images
 
760
  )
761
 
762
  return DownloadResponse(