Initial commit: Messy-Little-Gadgets v1.0.0
This commit is contained in:
+16
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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}")
|
||||
@@ -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("<Button-1>", 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 <a> tags
|
||||
music_links = soup.select('div.card.mb-1 a.music-link.d-block')
|
||||
|
||||
# Find all matching <span> 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 <td> tags (singers)
|
||||
singers = [td.get_text(strip=True) for td in soup.select('td.text-success')]
|
||||
|
||||
# Find all matching <a> 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()
|
||||
Reference in New Issue
Block a user