# Standard library packages
import re
import os
import sys
import shutil
import string
import argparse
import html
from urllib.parse import urlparse
import json
import ftplib
# Third-party packages
import requests
from bs4 import BeautifulSoup
from mutagen.flac import FLAC
from mutagen.mp3 import MP3
from torf import Torrent
from tqdm import tqdm
from langdetect import detect
# JPS-AU files
import smpy
def asciiart ():
print("""
███████╗███╗ ███╗ █████╗ ██╗ ██╗
██╔════╝████╗ ████║ ██╔══██╗██║ ██║
███████╗██╔████╔██║█████╗███████║██║ ██║
╚════██║██║╚██╔╝██║╚════╝██╔══██║██║ ██║
███████║██║ ╚═╝ ██║ ██║ ██║╚██████╔╝
╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝
""")
# Get arguments using argparse
def getargs():
parser = argparse.ArgumentParser()
parser.add_argument('-dir', '--directory', help='Initiate upload on directory.', nargs='?', required=True)
parser.add_argument("-f", "--freeleech", help="Enables freeleech.", action="store_true")
parser.add_argument("-t", "--tags", help="Add additional tags to the upload.", nargs='?')
parser.add_argument('-n', '--debug', help='Enable debug mode.', action='store_true')
parser.add_argument("-d", "--dryrun", help="Dryrun will carry out all actions other than the actual upload to JPS.", action="store_true")
parser.add_argument("-im", "--imageURL", help='Set the torrent cover URL.', nargs='?')
parser.add_argument("-a", "--artists", help='Set the artists. (Romaji\English)', nargs='?')
parser.add_argument("-ca", "--contributingartists", help='Set the contributing artists. (Romaji\English)', nargs='?')
parser.add_argument("-rt", "--releasetype", help='Set the release type.', nargs='?')
parser.add_argument("-ti", "--title", help='Set the title. (Romaji\English)', nargs='?')
parser.add_argument("-eti", "--editiontitle", help='Set the edition title', nargs='?')
parser.add_argument("-ey", "--editionyear", help='Set the torrent edition year (YYYYMMDD or YYYY).', nargs='?')
parser.add_argument("-ms", "--mediasource", help='Set the media source.', nargs='?')
return parser.parse_args()
# Acquire the authkey used for torrent files from upload.php
def getauthkey():
"""
Get SM session authkey for use by uploadtorrent() data dict.
Uses SM login data
:return: authkey
"""
smpage = sm.retrieveContent("https://sugoimusic.me/torrents.php?id=118") # Arbitrary page on JPS that has authkey
soup = BeautifulSoup(smpage.text, 'html5lib')
rel2 = str(soup.select('#content .thin .main_column .torrent_table tbody'))
authkey = re.findall('authkey=(.*)&torrent_pass=', rel2)
return authkey
def copytree(src, dst, symlinks=False, ignore=None):
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
shutil.copytree(s, d, symlinks, ignore)
else:
shutil.copy2(s, d)
# Creates torrent file using torf module.
def createtorrent(authkey, directory, filename, releasedata):
t = Torrent(path=directory,
trackers=[authkey]) # Torf requires we store authkeys in a list object. This makes it easier to add multiple announce urls.
# Set torrent to private as standard practice for private trackers
t.private = True
t.source = "SugoiMusic"
t.generate()
## Format releasedata to bring a suitable torrent name.
# The reason we don't just use the directory name is because of an error in POSTING.
# POSTS do not seem to POST hangul/jp characters alongside files.
# filename = f"{releasedata['idols[]']} - {releasedata['title']} [{releasedata['media']}-{releasedata['audioformat']}].torrent"
filename = f"{releasedata['title']} [{releasedata['media']}-{releasedata['audioformat']}].torrent"
filename = filename.replace("/","/")
filename = filename.replace(":",":")
filename = filename.replace("?","")
try:
t.write(filename)
print("_" * 100)
print("Torrent creation:\n")
print(f"{filename} has been created.")
except:
print("_" * 100)
print("Torrent creation:\n")
os.remove(filename)
print(f"{filename} already exists, existing torrent will be replaced.")
t.write(filename)
print(f"{filename} has been created.")
return filename
# Reads FLAC file and returns metadata.
def readflac(filename):
read = FLAC(filename)
# get some audio info
audio_info={
"SAMPLE_RATE": read.info.sample_rate,
"BIT_DEPTH": read.info.bits_per_sample
}
# Create dict containing all meta fields we'll be using.
tags={
"ALBUM": read.get('album'),
"ALBUMARTIST": read.get('albumartist'),
"ARTIST": read.get('artist'),
"DATE": read.get('date')[0],
"GENRE": "",#read.get('genre'),
"TITLE": read.get('title'),
"COMMENT": read.get('comment'),
"TRACKNUMBER": read.get('tracknumber')[0].zfill(2),
"DISCNUMBER": read.get('discnumber')}
# Not further looked into this but some FLACs hold a grouping key of contentgroup instead of grouping.
tags['GROUPING'] = read.get('grouping')
## If grouping returns None we check contentgroup.
# If it still returns none we will ignore it and handle on final checks
if tags['GROUPING'] == None:
tags['GROUPING'] = read.get('contentgroup')
required_tags = ['ALBUM', 'ALBUMARTIST','DATE','TRACKNUMBER']
for k,v in tags.items():
if v == None:
if k in required_tags:
print(f"{k} has returned {v}, this is a required tag")
sys.exit()
return tags, audio_info
# Reads MP3 file and returns metadata.
def readmp3(filename):
read = MP3(filename)
# Create dict containing all meta fields we'll be using.
tags={
"ALBUM": read.get('TALB'), # Album Title
"ALBUMARTIST": read.get('TPE2'), # Album Artist
"ARTIST": read.get('TPE1'), # Track Artist
"DATE": str(read.get('TDRC')), # Date YYYYMMDD (Will need to add a try/except for other possible identifiers)
"GENRE": read.get('TCON').text, # Genre
"TITLE": read.get('TIT2'), # Track Title
"COMMENT": read.get('COMM::eng'), # Track Comment
"GROUPING": read.get('TIT1'), # Grouping
"TRACKNUMBER": re.sub(r"\/.*", "", str(read.get('TRCK'))).zfill(2), # Tracknumber (Format #/Total) Re.sub removes /#
"DISCNUMBER": re.sub(r"\/.*", "", str(read.get('TPOS')))} # Discnumber (Format #/Total) Re.sub removes /#
required_tags = ['ALBUM', 'ALBUMARTIST','DATE','TRACKNUMBER']
for k,v in tags.items():
if v == None:
if k in required_tags:
print(f"{k} has returned {v}, this is a required tag")
sys.exit()
return tags
# Generates new log file based on directory contents
def generatelog(track_titles, log_filename, log_directory):
# Seperate each tracklist entry in the list with a newline
track_titles = '\n'.join([str(x) for x in track_titles])
# Format tracklist layout
log_contents = f"""[size=5][b]Tracklist[/b][/size]\n{track_titles}
"""
# If we have chosen to save the tracklist then we write log_contents to a .log file within the log directory specified
if cfg['local_prefs']['save_tracklist']:
# Write to {album_name}.log
with open(f"{log_directory}/{log_filename}.log", "w+") as f:
f.write(log_contents)
# Reset position to first line and read
f.seek(0)
log_contents = f.read()
f.close()
# If debug mode is enabled we will print the log contents.
if debug:
print("_" * 100)
print(f"Log Contents/Tracklisting: {log_contents}")
return log_contents
def readlog(log_name, log_directory):
with open(f"{log_directory}/{log_name}.log", "r+") as f:
log_contents = f.read()
f.close()
return log_contents
def add_to_hangul_dict(hangul , english , category):
hangul = str(hangul)
english = str(english)
categories = ['version','general','artist','genres', 'label', 'distr']
file = f"json_data/dictionary.json"
json_file = open(file, 'r', encoding='utf-8', errors='ignore')
dictionary = json.load(json_file)
json_file.close()
new = dict()
for cats in dictionary:
#== Create the categories in the new temp file
new[cats] = dict()
for key,value in dictionary[cats].items():
#== List all the old items into the new dict
new[cats][key] = value
if hangul in new[category].keys():
if new[category].get(hangul) is None:
if english != 'None':
new[category][hangul] = english
else:
#== Only update if English word has been supplied ==#
if english != 'None':
new[category][hangul] = english
else:
if english == 'None':
new[category][hangul] = None
else:
new[category][hangul] = english
json_write = open(file, 'w+', encoding='utf-8')
json_write.write(json.dumps(new, indent=4, ensure_ascii=False))
json_write.close()
def translate(string, category, result=None, output=None):
file = "json_data/dictionary.json"
with open(file, encoding='utf-8', errors='ignore') as f:
dictionary = json.load(f, strict=False)
category = str(category)
string = str(string)
search = dictionary[category]
string = string.strip()
if string == 'Various Artists':
output = ['Various Artists',None]
else:
#== NO NEED TO SEARCH - STRING HAS HANGUL+ENGLISH or HANGUL+HANGUL ==#
if re.search("\((?P
Invalid (.*)
', SMres.text) if len(SMerrorTorrent)!=0: print("Upload failed. Torrent error") print(SMerrorTorrent) # if len(SMerrorTorrent)!=0: # print("Upload failed. Logon error") # print(SMerrorLogon) if dryrun != True: print('\nUpload POSTED. It may take a moment for this upload to appear on SugoiMusic.') ## TODO Filter through JPSres.text and create error handling based on responses #print(JPSres.text) # Function for transferring the contents of the torrent as well as the torrent. def ftp_transfer(fileSource, fileDestination, directory, folder_name, watch_folder): # Create session session = ftplib.FTP(cfg['ftp_prefs']['ftp_server'],cfg['ftp_prefs']['ftp_username'],cfg['ftp_prefs']['ftp_password']) # Set session encoding to utf-8 so we can properly handle hangul/other special characters session.encoding='utf-8' # Successful FTP Login Print print("_" * 100) print("FTP Login Successful") print(f"Server Name: {cfg['ftp_prefs']['ftp_server']} : Username: {cfg['ftp_prefs']['ftp_username']}\n") if cfg['ftp_prefs']['add_to_downloads_folder']: # Create folder based on the directory name of the folder within the torrent. try: session.mkd(f"{fileDestination}/{folder_name}") print(f'Created directory {fileDestination}/{folder_name}') except ftplib.error_perm: pass # Notify user we are beginning the transfer. print(f"Beginning transfer...") # Set current folder to the users preferred destination session.cwd(f"{fileDestination}/{folder_name}") # Transfer each file in the chosen directory for file in os.listdir(directory): with open(f"{directory}/{file}",'rb') as f: filesize = os.path.getsize(f"{directory}/{file}") ## Transfer file # tqdm used for better user feedback. with tqdm(unit = 'blocks', unit_scale = True, leave = False, miniters = 1, desc = f'Uploading [{file}]', total = filesize) as tqdm_instance: session.storbinary('STOR ' + file, f, 2048, callback = lambda sent: tqdm_instance.update(len(sent))) print(f"{file} | Complete!") f.close() if cfg['ftp_prefs']['add_to_watch_folder']: with open(fileSource,'rb') as t: # Set current folder to watch directory session.cwd(watch_folder) ## Transfer file # We avoid tqdm here due to the filesize of torrent files. # Most connections will upload these within 1-3s, resulting in near useless progress bars. session.storbinary(f"STOR {torrentfile}", t) print(f"{torrentfile} | Sent to watch folder!") t.close() # Quit session when complete. session.quit() def localfileorganization(torrent, directory, watch_folder, downloads_folder): # Move torrent directory to downloads_folder if cfg['local_prefs']['add_to_downloads_folder']: try: os.mkdir(os.path.join(downloads_folder, os.path.basename(directory))) except FileExistsError: pass copytree(directory, os.path.join(downloads_folder, os.path.basename(directory))) shutil.rmtree(directory) if cfg['local_prefs']['add_to_watch_folder']: os.rename(torrent, f"{watch_folder}/{torrent}") if __name__ == "__main__": asciiart() args = getargs() with open(f'json_data/config.json') as f: cfg = json.load(f) # TODO consider calling args[] directly, we will then not need this line dryrun = freeleech = tags = directory = debug = imageURL = artists = contributingartists = releasetype = title = editiontitle = editionyear = mediasource = audio_info = None directory = args.directory additional_tags = args.tags if args.dryrun: dryrun = True if args.debug: debug = True if args.freeleech: freeleech = True if args.imageURL: imageURL = args.imageURL if args.releasetype: releasetype = args.releasetype if args.title: title = args.title if args.artists: artists = args.artists if args.contributingartists: contributingartists = args.contributingartists if args.editiontitle: editiontitle = args.editiontitle if args.editionyear: editionyear = args.editionyear if args.mediasource: mediatype = args.mediasource # Load login credentials from JSON and use them to create a login session. loginData = {'username': cfg['credentials']['username'], 'password': cfg['credentials']['password']} loginUrl = "https://sugoimusic.me/login.php" loginTestUrl = "https://sugoimusic.me" successStr = "Enabled users" passkey = cfg['credentials']['passkey'] annouceurl = "https://tracker.sugoimusic.me:24601/"+passkey+"/announce" # j is an object which can be used to make requests with respect to the loginsession sm = smpy.MyLoginSession(loginUrl, loginData, loginTestUrl, successStr, debug=args.debug) # Acquire authkey authkey = getauthkey() # Gather data of FLAC file releasedata = gatherdata(directory) # Folder_name equals the last folder in the path, this is used to rename .torrent files to something relevant. folder_name = os.path.basename(os.path.normpath(directory)) # Identifying cover.jpg path # cover_path = directory + "/" + cfg['local_prefs']['cover_name'] # Create torrent file. #torrentfile = createtorrent(authkey, directory, folder_name, releasedata) torrentfile = createtorrent(annouceurl, directory, folder_name, releasedata) # Upload torrent to SugoiMusic uploadtorrent(torrentfile, imageURL, releasedata) # Setting variable for watch/download folders ftp_watch_folder = cfg['ftp_prefs']['ftp_watch_folder'] ftp_downloads_folder = cfg['ftp_prefs']['ftp_downloads_folder'] local_watch_folder = cfg['local_prefs']['local_watch_folder'] local_downloads_folder = cfg['local_prefs']['local_downloads_folder'] if not dryrun: if cfg['ftp_prefs']['enable_ftp']: ftp_transfer(fileSource=torrentfile, fileDestination=ftp_downloads_folder, directory=directory, folder_name=folder_name, watch_folder=ftp_watch_folder) if cfg['local_prefs']['add_to_watch_folder'] or cfg['local_prefs']['add_to_downloads_folder']: localfileorganization(torrent=torrentfile, directory=directory, watch_folder=local_watch_folder, downloads_folder=local_downloads_folder)