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