diff --git a/README.md b/README.md index 1122cbf..7252c14 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,22 @@ A secure password hashing utility using **Argon2id** (2025 recommended parameter --- +## 🖼️ Bing Wallpaper + +A self-contained sub-suite for Bing wallpaper tools (in the `bing_wallpaper/` folder): +- **Bing Wallpaper Downloader** — batch download Bing daily wallpapers (async, resume) +- **Daily Wallpaper** — daily desktop wallpaper rotation from a folder + +```bash +cd bing_wallpaper +python bing_wallpaper_downloader.py # download Bing wallpapers +python daily_wallpaper.py # rotate desktop wallpaper daily +``` + +See `bing_wallpaper/README.md` for details. + +--- + ## 📁 Project Structure ``` @@ -78,6 +94,10 @@ Messy-Little-Gadgets/ ├── music_downloader.py # Music download GUI tool ├── argon2_password_hasher.py # Argon2 password hasher ├── image_resolution_scanner.py # Image resolution scanner +├── bing_wallpaper/ # 🖼️ Bing wallpaper tools +│ ├── bing_wallpaper_downloader.py +│ ├── daily_wallpaper.py +│ └── README.md └── README.md # This document ``` diff --git a/bing_wallpaper/README.md b/bing_wallpaper/README.md new file mode 100644 index 0000000..b63ed5a --- /dev/null +++ b/bing_wallpaper/README.md @@ -0,0 +1,48 @@ +# 🖼️ Bing Wallpaper Suite + +> A small suite of **Bing wallpaper** tools: batch download Bing daily wallpapers, and rotate them as your desktop wallpaper daily. + +This folder contains two standalone tools for Bing wallpapers. + +--- + +## 🧩 Tools + +### ⬇️ Bing Wallpaper Downloader — `bing_wallpaper_downloader.py` +Batch download Bing daily wallpapers from `peapix.com`. +- ⚡ Async concurrent download (aiohttp, 50 concurrent) +- 🔄 Resume: skips already-downloaded files +- 📈 Progress & speed reporting + +```bash +pip install aiohttp aiofiles +python bing_wallpaper_downloader.py +``` + +### 🔁 Daily Wallpaper — `daily_wallpaper.py` +Rotate your Windows desktop wallpaper daily from a folder of images. +- 📁 Reads wallpaper directory from config (prompts on first run) +- 🔄 Cycles through images day by day (rotates at the end) +- 🖥️ Sets wallpaper via Win32 API +- 📄 Creates an `ok_.txt` run record + +```bash +python daily_wallpaper.py +``` + +--- + +## 📁 Structure + +``` +bing_wallpaper/ +├── bing_wallpaper_downloader.py # Batch download Bing wallpapers +├── daily_wallpaper.py # Daily wallpaper rotation +└── README.md # This file +``` + +--- + +## 📄 License + +Part of **Messy-Little-Gadgets** (MIT License). diff --git a/bing_wallpaper/bing_wallpaper_downloader.py b/bing_wallpaper/bing_wallpaper_downloader.py new file mode 100644 index 0000000..cd5f515 --- /dev/null +++ b/bing_wallpaper/bing_wallpaper_downloader.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- +""" +Bing Wallpaper Downloader +============================================================ +Batch download Bing daily wallpapers from peapix.com. +- Async concurrent download (aiohttp) +- Resume: skips already-downloaded files +- Progress & stats reporting + +Usage: python bing_wallpaper_downloader.py +""" +import re +import os +import time +import asyncio +import aiohttp +import aiofiles +from urllib.parse import urlparse + +# Global state +downloaded_urls = set() +downloaded_ids = set() +semaphore = None +print_lock = asyncio.Lock() + + +def load_existing_files(download_dir="bing_images"): + """Load existing files to avoid re-downloading.""" + global downloaded_urls, downloaded_ids + if not os.path.exists(download_dir): + return + + for filename in os.listdir(download_dir): + if filename.endswith('.jpg'): + # Extract ID from filename + match = re.search(r'(\d{5})', filename) + if match: + downloaded_ids.add(int(match.group(1))) + downloaded_urls.add(filename) + + print(f"Loaded {len(downloaded_ids)} existing images") + + +async def download_image(session, url, download_dir="bing_images"): + """Download and save an image asynchronously.""" + if url in downloaded_urls: + return None + + try: + headers = { + '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', + 'Referer': 'https://peapix.com/' + } + + async with session.get(url, headers=headers, timeout=30) as resp: + if resp.status != 200: + return None + + # Check content size + content_length = resp.headers.get('content-length') + if content_length and int(content_length) < 5 * 1024: + return None + + # Generate filename and path + filename = os.path.basename(urlparse(url).path) + filepath = os.path.join(download_dir, filename) + + # Skip if file already exists + if os.path.exists(filepath): + downloaded_urls.add(url) + return filepath + + # Stream download and save + async with aiofiles.open(filepath, 'wb') as f: + async for chunk in resp.content.iter_chunked(8192): # 8KB per chunk + await f.write(chunk) + + # Validate file size (reject error pages) + if os.path.getsize(filepath) < 5 * 1024: + os.remove(filepath) + return None + + downloaded_urls.add(url) + return filepath + + except Exception as e: + return None + + +async def process_one_page(session, page_id, download_dir="bing_images"): + """Process a single page: extract the image URL and download it.""" + # Skip if already downloaded + if page_id in downloaded_ids: + return "skipped" + + page_url = f"https://peapix.com/bing/{page_id}" + + try: + headers = { + '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' + } + + # Get page HTML + async with session.get(page_url, headers=headers, timeout=15) as resp: + if resp.status != 200: + return None + html = await resp.text() + + # Extract image URL + pattern = r']+src="(https://img\.peapix\.com/[^"]+_1280\.jpg)"' + matches = re.findall(pattern, html) + + if not matches: + return None + + img_url = matches[0] + original_url = img_url.replace("_1280", "") + + # Skip if URL already downloaded + if original_url in downloaded_urls: + return "skipped" + + # Show download status + async with print_lock: + print(f"[{page_id}] 📥 Downloading...") + + # Download the image + result = await download_image(session, original_url, download_dir) + + if result: + downloaded_ids.add(page_id) + async with print_lock: + filename = os.path.basename(result) + file_size = os.path.getsize(result) / 1024 + print(f"[{page_id}] ✅ Saved: {filename} ({file_size:.1f}KB)") + else: + async with print_lock: + print(f"[{page_id}] ❌ Download failed") + + return result + + except asyncio.TimeoutError: + async with print_lock: + print(f"[{page_id}] ⏰ Timeout") + return None + except Exception as e: + async with print_lock: + print(f"[{page_id}] ❌ Error: {str(e)[:30]}") + return None + + +async def worker(session, task_id, download_dir, results): + """Worker coroutine controlled by the semaphore.""" + async with semaphore: + result = await process_one_page(session, task_id, download_dir) + results[task_id] = result + + +async def async_batch_download(start_id=51418, end_id=56807, max_concurrent=50, download_dir="bing_images"): + """Main async batch download function.""" + os.makedirs(download_dir, exist_ok=True) + + load_existing_files(download_dir) + + global semaphore + semaphore = asyncio.Semaphore(max_concurrent) + + total = end_id - start_id + 1 + print("=" * 60) + print(f"📊 Task: {start_id} -> {end_id} ({total} items)") + print(f"⚡ Concurrency: {max_concurrent}") + print(f"📁 Save dir: {download_dir}") + print(f"💾 Existing: {len(downloaded_ids)}") + print("=" * 60) + print() + + start_time = time.time() + results = {} + tasks = list(range(start_id, end_id + 1)) + + connector = aiohttp.TCPConnector( + limit=max_concurrent * 2, + limit_per_host=max_concurrent, + ttl_dns_cache=300 + ) + timeout = aiohttp.ClientTimeout(total=30, connect=10) + + async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session: + task_list = [ + worker(session, task_id, download_dir, results) + for task_id in tasks + ] + + batch_size = 100 + completed = 0 + + for i in range(0, len(task_list), batch_size): + batch = task_list[i:i + batch_size] + await asyncio.gather(*batch, return_exceptions=True) + + completed += len(batch) + elapsed = time.time() - start_time + speed = completed / elapsed if elapsed > 0 else 0 + + async with print_lock: + print(f"\n{'='*60}") + print(f"📈 Progress: {completed}/{total} ({completed/total*100:.1f}%)") + print(f"⚡ Speed: {speed:.1f} items/sec") + print(f"⏱️ Elapsed: {elapsed:.1f}s") + print(f"{'='*60}\n") + + # Stats + success_count = 0 + skipped_count = 0 + failed_ids = [] + + for task_id, result in results.items(): + if result == "skipped": + skipped_count += 1 + elif result: + success_count += 1 + else: + failed_ids.append(task_id) + + elapsed = time.time() - start_time + + print("\n" + "=" * 60) + print("📊 Download summary:") + print(f" ✅ Success: {success_count}") + print(f" ⏭️ Skipped (exists): {skipped_count}") + print(f" ❌ Failed: {len(failed_ids)}") + if failed_ids: + print(f" 🔴 Failed IDs (first 20): {failed_ids[:20]}") + print(f" ⏱️ Total time: {elapsed:.1f}s") + print(f" 📈 Avg speed: {total/elapsed:.1f} items/sec") + print(f" 📁 Save dir: {os.path.abspath(download_dir)}") + print("=" * 60) + + +def batch_download(start_id=51418, end_id=56807, max_concurrent=50, download_dir="bing_images"): + """Synchronous entry point.""" + asyncio.run(async_batch_download( + start_id=start_id, + end_id=end_id, + max_concurrent=max_concurrent, + download_dir=download_dir + )) + + +if __name__ == "__main__": + batch_download( + start_id=51418, + end_id=56807, + max_concurrent=50, + download_dir="bing_images" + ) diff --git a/bing_wallpaper/daily_wallpaper.py b/bing_wallpaper/daily_wallpaper.py new file mode 100644 index 0000000..93c587d --- /dev/null +++ b/bing_wallpaper/daily_wallpaper.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +""" +Daily Wallpaper +============================================================ +Rotate the Windows desktop wallpaper daily from a folder of images. +- Reads wallpaper directory from config (or prompts on first run) +- Cycles through images day by day (rotates when reaching the end) +- Sets the wallpaper via Win32 API +- Creates an ok_.txt run record + +Usage: python daily_wallpaper.py +""" +import ctypes +import os +import json +from datetime import datetime +import sys + +STATE_FILE = "wallpaper_state.json" +CONFIG_FILE = "wallpaper_config.json" + + +def get_script_dir(): + """Return the script/exe directory.""" + if getattr(sys, 'frozen', False): + return os.path.dirname(os.path.abspath(sys.executable)) + else: + return os.path.dirname(os.path.abspath(__file__)) + + +def get_today(): + return datetime.now().strftime("%Y-%m-%d") + + +def get_timestamp(): + """Return a current timestamp string.""" + return datetime.now().strftime("%Y%m%d_%H%M%S") + + +def create_ok_file(wallpaper_dir, wallpaper_file, index, total): + """Create an ok_.txt file recording the run status.""" + script_dir = get_script_dir() + timestamp = get_timestamp() + ok_file_path = os.path.join(script_dir, f"ok_{timestamp}.txt") + + content = f"""Wallpaper updated +Run time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +Wallpaper dir: {wallpaper_dir} +Today's wallpaper: {wallpaper_file} +Index: {index}/{total} +""" + try: + with open(ok_file_path, 'w', encoding='utf-8') as f: + f.write(content) + print(f"✅ Run record saved: {ok_file_path}") + return True + except Exception as e: + print(f"⚠️ Failed to save run record: {e}") + return False + + +def get_wallpaper_dir(): + """Read the wallpaper path from config; prompt & save on first run.""" + script_dir = get_script_dir() + config_path = os.path.join(script_dir, CONFIG_FILE) + + # Try reading the JSON config + if os.path.exists(config_path): + try: + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + path = config.get('wallpaper_dir', '') + if path and os.path.exists(path): + print(f"✅ Read path from config: {path}") + return path + else: + print(f"⚠️ Config path is invalid: {path}") + except json.JSONDecodeError: + print("⚠️ Config file corrupted, will re-configure") + else: + print(f"📄 Config file not found: {config_path}, first-time config") + + # No config or invalid path: prompt the user + while True: + path = input("📁 Enter wallpaper directory path: ").strip().strip('"') + + if os.path.exists(path): + config = { + 'wallpaper_dir': path, + 'last_updated': get_today() + } + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, ensure_ascii=False, indent=2) + print(f"✅ Config saved: {config_path}") + return path + else: + print(f"❌ Directory does not exist: {path}, please re-enter") + + +def get_start_date(wallpaper_dir): + """Read the start date from the state JSON file.""" + state_path = os.path.join(wallpaper_dir, STATE_FILE) + + if os.path.exists(state_path): + try: + with open(state_path, 'r', encoding='utf-8') as f: + state = json.load(f) + return state.get('start_date') + except (json.JSONDecodeError, KeyError): + print("⚠️ State file corrupted, will re-initialize") + + return None + + +def save_start_date(wallpaper_dir, start_date): + """Save the start date to a JSON file.""" + state_path = os.path.join(wallpaper_dir, STATE_FILE) + state = { + 'start_date': start_date, + 'last_updated': get_today() + } + with open(state_path, 'w', encoding='utf-8') as f: + json.dump(state, f, ensure_ascii=False, indent=2) + + +def get_image_files(wallpaper_dir): + """Return all image files in the directory, sorted by name.""" + files = [] + for f in os.listdir(wallpaper_dir): + if f.lower().endswith(('.jpg', '.jpeg', '.png', '.webp', '.bmp')): + files.append(f) + files.sort() + return files + + +def get_next_wallpaper(wallpaper_dir): + """Calculate which wallpaper to use today.""" + files = get_image_files(wallpaper_dir) + + if not files: + print("❌ No images found in wallpaper directory") + return None, 0, 0 + + total = len(files) + today = get_today() + today_dt = datetime.strptime(today, "%Y-%m-%d") + + # Read the start date + start_date = get_start_date(wallpaper_dir) + + # First run: record today as the start date + if start_date is None: + start_date = today + current_index = 1 + print(f"📝 First run, start date: {start_date}") + else: + # Calculate days since the start date + start_dt = datetime.strptime(start_date, "%Y-%m-%d") + days_diff = (today_dt - start_dt).days + current_index = days_diff + 1 + print(f"📅 Start: {start_date}, Today: {today}, Day {current_index}") + + # Rotate if past the total + if current_index > total: + current_index = ((current_index - 1) % total) + 1 + print(f"🔄 Looped, current index: {current_index}") + + # Save the start date + save_start_date(wallpaper_dir, start_date) + + wallpaper_file = files[current_index - 1] + wallpaper_path = os.path.join(wallpaper_dir, wallpaper_file) + + print(f"📸 Today's wallpaper: {wallpaper_file} ({current_index}/{total})") + return wallpaper_path, current_index, total + + +def set_wallpaper(image_path): + """Set the desktop wallpaper.""" + abs_path = os.path.abspath(image_path) + + SPI_SETDESKWALLPAPER = 20 + SPIF_UPDATEINIFILE = 0x01 + SPIF_SENDWININICHANGE = 0x02 + SPIF_FLAGS = SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE + + try: + ctypes.windll.user32.SystemParametersInfoW( + SPI_SETDESKWALLPAPER, + 0, + abs_path, + SPIF_FLAGS + ) + print(f"✅ Wallpaper set: {os.path.basename(image_path)}") + return True + except Exception as e: + print(f"❌ Failed to set wallpaper: {e}") + return False + + +def main(): + print("=" * 50) + print("🖼️ Daily Wallpaper Rotation") + print("=" * 50) + + # Get wallpaper directory + wallpaper_dir = get_wallpaper_dir() + print(f"📁 Wallpaper dir: {wallpaper_dir}") + + # Get image count + files = get_image_files(wallpaper_dir) + print(f"📊 Total wallpapers: {len(files)}") + + # Calculate and set today's wallpaper + wallpaper_path, current_index, total = get_next_wallpaper(wallpaper_dir) + + if wallpaper_path and os.path.exists(wallpaper_path): + if set_wallpaper(wallpaper_path): + # After setting, create the ok_.txt file + wallpaper_file = os.path.basename(wallpaper_path) + create_ok_file(wallpaper_dir, wallpaper_file, current_index, total) + else: + print("❌ Wallpaper file does not exist") + + print("=" * 50) + print("👋 Done!") + sys.exit(0) + + +if __name__ == "__main__": + main()