commit 490381c5c6c37d02ed4d7b1892ec4126108b0c3e Author: dvs-dvsxt Date: Fri Aug 28 10:51:16 2026 +0800 Initial commit: Messy-Little-Gadgets v1.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3737373 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.venv/ +venv/ +env/ +*.mp3 +*.wav + +# System files +Thumbs.db +.DS_Store +desktop.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fddd2dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DVS (dvs-dvsxt) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..18338c5 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# 🧰 Messy-Little-Gadgets + +> A collection of small, handy Python utilities — random little tools for everyday tasks. + +**Messy-Little-Gadgets** is a growing collection of miscellaneous Python scripts and tools. Each script is self-contained and solves a simple, practical problem. + +--- + +## 🧩 Included Tools + +### 🎵 Music Downloader — `music_downloader.py` +A simple Tkinter GUI tool to search and download music from an online source. +- 🔍 **Search** songs by keyword +- ⬇️ **Download** a song by its ID +- 🔥 **Hot songs** list browsing +- 📂 **Custom save path** via file dialog + +```bash +python music_downloader.py +``` + +### 🔐 Argon2 Password Hasher — `argon2_password_hasher.py` +A secure password hashing utility using **Argon2id** (2025 recommended parameters). +- 🧂 **Auto salt** generation +- 💾 **Hash** passwords (Argon2id format) +- ✔️ **Verify** passwords +- 🔄 **Rehash check** for parameter updates + +```bash +python argon2_password_hasher.py +``` + +--- + +## 📁 Project Structure + +``` +Messy-Little-Gadgets/ +├── music_downloader.py # Music download GUI tool +├── argon2_password_hasher.py # Argon2 password hasher +└── README.md # This document +``` + +--- + +## 📄 License + +This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details. + +--- + +## 🙏 Note + +> Each gadget is independent and minimal. More little tools will be added over time. Feel free to use them in your own projects. diff --git a/argon2_password_hasher.py b/argon2_password_hasher.py new file mode 100644 index 0000000..a5c888d --- /dev/null +++ b/argon2_password_hasher.py @@ -0,0 +1,60 @@ +from argon2 import PasswordHasher, exceptions +import secrets + +class Argon2Hasher: + def __init__(self): + """ + Recommended Argon2id configuration parameters (2025 standard): + - time_cost: Number of iterations (CPU cost) + - memory_cost: Memory usage in KB + - parallelism: Number of parallel threads + - hash_len: Hash output length + - salt_len: Salt length + """ + self.ph = PasswordHasher( + time_cost=3, # Modern hardware: 2-3, older hardware: 1 + memory_cost=65536, # 64 MB (in KB) + parallelism=4, # 4 threads + hash_len=32, # 32 bytes = 256 bits + salt_len=16, # 16 bytes = 128 bits + ) + + def hash_password(self, password: str) -> str: + """ + Hash a password (automatically generates a random salt) + Returns format: $argon2id$v=19$m=65536,t=3,p=4$salt$hash + """ + return self.ph.hash(password) + + def verify_password(self, hashed_password: str, password: str) -> bool: + """ + Verify a password against its hash + """ + try: + return self.ph.verify(hashed_password, password) + except (exceptions.VerifyMismatchError, exceptions.VerificationError): + return False + + def needs_rehash(self, hashed_password: str) -> bool: + """ + Check if the password needs to be rehashed (used when parameters are updated) + """ + return self.ph.check_needs_rehash(hashed_password) + +# Usage example +if __name__ == "__main__": + hasher = Argon2Hasher() + + # 1. Hash a password + password = "Lp324123" # This is just an example; use a stronger password in production! + hashed = hasher.hash_password(password) + print(f"Hashed: {hashed}") + + # 2. Verify the password + test_password = "Lp324123" + is_valid = hasher.verify_password(hashed, test_password) + print(f"Password valid: {is_valid}") + + # 3. Check if rehashing is needed + needs_rehash = hasher.needs_rehash(hashed) + print(f"Needs rehash: {needs_rehash}") \ No newline at end of file diff --git a/music_downloader.py b/music_downloader.py new file mode 100644 index 0000000..7887f6e --- /dev/null +++ b/music_downloader.py @@ -0,0 +1,182 @@ +import requests +from bs4 import BeautifulSoup +import tkinter as tk +from tkinter import filedialog +import re +import os + +class MusicDownloader: + def __init__(self, master): + self.master = master + master.title("Music Downloader") + + # Search song section + tk.Label(master, text="Song keyword:").grid(row=0, column=0, padx=10, pady=10) + self.keyword_entry = tk.Entry(master, width=30) + self.keyword_entry.grid(row=0, column=1, padx=10, pady=10) + self.search_button = tk.Button(master, text="Search", command=self.search_song) + self.search_button.grid(row=0, column=2, padx=10, pady=10) + + # Text box to show search results + self.result_text = tk.Text(master, height=10, width=50) + self.result_text.grid(row=1, column=0, columnspan=3, padx=10, pady=10) + # Hyperlink hint + self.hint_label = tk.Label(master, text="Not the result you want?", fg="blue", cursor="hand2") + self.hint_label.grid(row=2, column=0, columnspan=10, pady=10) + self.hint_label.bind("", self.show_search_hint) + # Download song section + tk.Label(master, text="Song ID:").grid(row=3, column=0, padx=10, pady=10) + self.song_id_entry = tk.Entry(master, width=30) + self.song_id_entry.grid(row=3, column=1, padx=10, pady=10) + self.download_button = tk.Button(master, text="Download", command=self.download_song) + self.download_button.grid(row=3, column=2, padx=10, pady=10) + + # Save path section + tk.Label(master, text="Save path:").grid(row=4, column=0, padx=10, pady=10) + self.save_path_entry = tk.Entry(master, width=30) + self.save_path_entry.grid(row=4, column=1, padx=10, pady=10) + self.browse_button = tk.Button(master, text="Browse", command=self.browse_save_path) + self.browse_button.grid(row=4, column=2, padx=10, pady=10) + # Hot music recommendation section + self.hot_music_button = tk.Button(master, text="Hot Songs", command=self.get_hot_music) + self.hot_music_button.grid(row=5, column=0, padx=10, pady=10) + + def search_song(self): + keyword = self.keyword_entry.get() + details = self.get_music_details(keyword) + self.result_text.delete(1.0, tk.END) # clear the text box + for detail in details: + self.result_text.insert(tk.END, detail + "\n") + + def get_music_details(self, keyword): + base_url = "https://www.gequbao.com/s/" + search_url = f"{base_url}{keyword}" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0' + } + + try: + response = requests.get(search_url, headers=headers) + response.raise_for_status() # check if the request succeeded + soup = BeautifulSoup(response.text, 'html.parser') + + # Find all matching tags + music_links = soup.select('div.card.mb-1 a.music-link.d-block') + + # Find all matching tags + music_titles = soup.select('span.text-primary.font-weight-bolder.music-title.d-md-inline-block.align-middle span') + + # Extract all href attributes, removing the '/music/' prefix + hrefs = [link['href'].replace('/music/', '') for link in music_links] + + # Extract all song titles + titles = [title.get_text(strip=True) for title in music_titles] + + # Combine titles and links + results = [f"Title: {title}, ID: {href}" for title, href in zip(titles, hrefs)] + + return results + except requests.RequestException as e: + print(f"Request failed, please try again later: {e}") + return [] + + def download_song(self): + in_id = self.song_id_entry.get() + save_path = self.save_path_entry.get() + + # Check whether the save path exists + if not save_path or not os.path.exists(save_path): + tk.messagebox.showwarning("Warning", "Save path does not exist. Please choose a valid path.") + return + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0' + } + url = f"https://www.gequbao.com/music/{in_id}" + + try: + resp = requests.get(url, headers=headers, timeout=5) # set timeout to 5 seconds + except requests.exceptions.Timeout: + tk.messagebox.showwarning("Warning", "Request timed out. Please check your network.") + return + except requests.exceptions.RequestException as e: + tk.messagebox.showwarning("Warning", f"Request failed, please try again later: {e}") + return + + + link = "https://www.gequbao.com/api/play-url" + data = {'id':'RFALUCRUAVZeSVsNV3NRXF5fF1pdAnhXU10DE1gPU3IaUVtRQlFNVndQUFpWRF9YUQ=='} + + try: + response = requests.post(url=link, data=data, headers=headers, timeout=5) # set timeout to 5 seconds + json_data = response.json() + play_url = json_data['data']['url'] + except requests.exceptions.Timeout: + tk.messagebox.showwarning("Warning", "Request timed out. Please check your network.") + return + except requests.exceptions.RequestException as e: + tk.messagebox.showwarning("Warning", f"Request failed, please try again later: {e}") + return + + try: + content = requests.get(play_url, headers=headers, timeout=5).content # set timeout to 5 seconds + except requests.exceptions.Timeout: + tk.messagebox.showwarning("Warning", "Request timed out. Please check your network.") + return + except requests.exceptions.RequestException as e: + tk.messagebox.showwarning("Warning", f"Request failed, please try again later: {e}") + return + + # Save the song + file_path = os.path.join(save_path, f"ruyuan.mp3") + with open(file_path, 'wb') as f: + f.write(content) + + self.result_text.insert(tk.END, f'Song downloaded successfully\n') + + def browse_save_path(self): + path = filedialog.askdirectory() + self.save_path_entry.delete(0, tk.END) + self.save_path_entry.insert(0, path) + + def get_hot_music(self): + # clear the text box + self.result_text.delete(1.0, tk.END) + + base_url = "https://www.gequbao.com/hot-music/" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0' + } + + results = [] + for page in range(1, 7): + url = f"{base_url}{page}" + try: + response = requests.get(url, headers=headers) + response.raise_for_status() # check if the request succeeded + soup = BeautifulSoup(response.text, 'html.parser') + + # Find all matching tags (singers) + singers = [td.get_text(strip=True) for td in soup.select('td.text-success')] + + # Find all matching tags + music_links = soup.select('a.text-info.font-weight-bold') + titles = [a.get_text(strip=True) for a in music_links] + hrefs = [a['href'].replace('/music/', '') for a in music_links] + + # Combine title, singer and ID + page_results = [f"Title: {title}, Singer: {singer}, ID: {href}" for title, singer, href in zip(titles, singers, hrefs)] + results.extend(page_results) + except requests.RequestException as e: + tk.messagebox.showwarning("Warning", f"Request failed, please try again later: {e}") + return + for result in results: + self.result_text.insert(tk.END, result + "\n") + + def show_search_hint(self, event): + tk.messagebox.showinfo("Search Hint", "Avoid special characters when searching. Enter the name and author together for better matching (e.g. ru yuan Wang Fei)") + +if __name__ == "__main__": + root = tk.Tk() + app = MusicDownloader(root) + root.mainloop()