mirror of
https://codeberg.org/mclemens/qrzlogger.git
synced 2024-12-23 00:16:25 -05:00
4dad3d2d2f
the user can now cancel the QSO entering process added callsign plausability check
416 lines
15 KiB
Python
416 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# qrzlogger
|
|
# =========
|
|
#
|
|
# This script is a QRZ.com command line QSO logger.
|
|
# It does the following:
|
|
# 1) asks the user for a call sign
|
|
# 2) displays available call sign info pulled from QRZ.com
|
|
# 3) displays all previous QSOs with this call (pulled from QRZ.com logbook)
|
|
# 4) alles the user to enter QSO specific data (date, time, report, band etc.)
|
|
# 5) uploads the QSO to QRZ.com's logbook
|
|
# 6) lists the last 5 logged QSOs ((pulled from QRZ.com logbook)
|
|
# 7) starts again from 1)
|
|
#
|
|
#
|
|
# MIT License
|
|
#
|
|
# Copyright (c) 2021 Michael Clemens, DL6MHC
|
|
#
|
|
# 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.
|
|
|
|
|
|
# WARNING: This software is beta and is really not working properly yet!
|
|
# I'll remove this warning when it's done
|
|
|
|
|
|
import requests
|
|
import urllib
|
|
import re
|
|
import datetime
|
|
import os
|
|
import xmltodict
|
|
from prettytable import PrettyTable
|
|
from requests.structures import CaseInsensitiveDict
|
|
from datetime import date
|
|
from datetime import timezone
|
|
import configparser
|
|
from colored import fore, back, style
|
|
|
|
# read the config file
|
|
config = configparser.ConfigParser()
|
|
config.read('config.ini')
|
|
|
|
# headers for all POST requests
|
|
headers = CaseInsensitiveDict()
|
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
|
|
session = None
|
|
session_key = None
|
|
|
|
# Read user definable colors from config
|
|
if config['qrzlogger']['use_colors'] == "yes":
|
|
inputcol = eval(config['qrzlogger']['inputcol'])
|
|
hlcol = eval(config['qrzlogger']['hlcol'])
|
|
defvalcol = eval(config['qrzlogger']['defvalcol'])
|
|
errorcol = eval(config['qrzlogger']['errorcol'])
|
|
successcol = eval(config['qrzlogger']['successcol'])
|
|
tablecol = eval(config['qrzlogger']['tablecol'])
|
|
logocol = eval(config['qrzlogger']['logocol'])
|
|
else:
|
|
inputcol = style.RESET
|
|
hlcol = style.RESET
|
|
defvalcol = style.RESET
|
|
errorcol = style.RESET
|
|
successcol = style.RESET
|
|
tablecol = style.RESET
|
|
logocol = style.RESET
|
|
|
|
|
|
# Generate a session for QRZ.com's xml service with
|
|
# the help of the QRZ.com username and password
|
|
def get_session():
|
|
global session
|
|
global session_key
|
|
xml_auth_url = '''https://xmldata.QRZ.com/xml/current/?username={0}&password={1}'''.format(
|
|
config['qrzlogger']['qrz_user'],config['qrzlogger']['qrz_pass'])
|
|
session = requests.Session()
|
|
session.verify = bool(os.getenv('SSL_VERIFY', True))
|
|
r = session.get(xml_auth_url)
|
|
if r.status_code == 200:
|
|
raw_session = xmltodict.parse(r.content)
|
|
session_key = raw_session.get('QRZDatabase').get('Session').get('Key')
|
|
if session_key:
|
|
return True
|
|
return False
|
|
|
|
|
|
# Query QRZ.com's xml api to gather information
|
|
# about a specific call sign
|
|
def getCallData(call):
|
|
global session
|
|
global session_key
|
|
|
|
xml_url = """https://xmldata.QRZ.com/xml/current/?s={0}&callsign={1}""" .format(session_key, call)
|
|
r = session.get(xml_url)
|
|
raw = xmltodict.parse(r.content).get('QRZDatabase')
|
|
calldata = raw.get('Callsign')
|
|
if calldata:
|
|
return calldata
|
|
return None
|
|
|
|
|
|
# Query QRZ.com's logbook for all previous QSOs
|
|
# with a specific call sign
|
|
def getQSOsForCallsign(callsign):
|
|
post_data = {
|
|
'KEY' : config['qrzlogger']['api_key'],
|
|
'ACTION' : 'FETCH',
|
|
'OPTION' : "TYPE:ADIF,CALL:" + callsign
|
|
}
|
|
post_data_enc = urllib.parse.urlencode(post_data)
|
|
|
|
resp = requests.post(config['qrzlogger']['api_url'], headers=headers, data=post_data_enc)
|
|
|
|
str_resp = resp.content.decode("utf-8")
|
|
response = urllib.parse.unquote(str_resp)
|
|
|
|
resp_list = response.splitlines()
|
|
result = [{}]
|
|
for i in resp_list:
|
|
if not i:
|
|
result.append({})
|
|
else:
|
|
if any(s+":" in i for s in config['qrzlogger']['xml_fields']):
|
|
i = re.sub('<','',i, flags=re.DOTALL)
|
|
i = re.sub(':.*>',":",i, flags=re.DOTALL)
|
|
v = re.sub('^.*:',"",i, flags=re.DOTALL)
|
|
k = re.sub(':.*$',"",i, flags=re.DOTALL)
|
|
result[-1][k] = v
|
|
return result
|
|
|
|
|
|
# Generate a pretty ascii table containing all
|
|
# previous QSOs with a specific call sign
|
|
def getQSOTable(result):
|
|
t = PrettyTable(['Date', 'Time', 'Band', 'Mode', 'RST-S', 'RST-R', 'Comment'])
|
|
for d in result:
|
|
if "qso_date" in d:
|
|
date = datetime.datetime.strptime(d["qso_date"], '%Y%m%d').strftime('%Y/%m/%d')
|
|
time = datetime.datetime.strptime(d["time_on"], '%H%M').strftime('%H:%M')
|
|
comment = ""
|
|
try:
|
|
comment = d["comment"]
|
|
except:
|
|
comment = ""
|
|
t.add_row([date, time, d["band"], d["mode"], d["rst_sent"], d["rst_rcvd"], comment])
|
|
t.align = "r"
|
|
return t
|
|
|
|
|
|
# Print a pretty ascii table containing all interesting
|
|
# data found for a specific call sign
|
|
def getXMLQueryTable(result):
|
|
t = PrettyTable(['key', 'value'])
|
|
if "fname" in result:
|
|
t.add_row(["First Name", result["fname"]])
|
|
if "name" in result:
|
|
t.add_row(["Last Name", result["name"]])
|
|
if "addr1" in result:
|
|
t.add_row(["Street", result["addr1"]])
|
|
if "addr2" in result:
|
|
t.add_row(["City", result["addr2"]])
|
|
if "state" in result:
|
|
t.add_row(["State", result["state"]])
|
|
if "country" in result:
|
|
t.add_row(["Country", result["country"]])
|
|
if "grid" in result:
|
|
t.add_row(["Locator", result["grid"]])
|
|
if "email" in result:
|
|
t.add_row(["Email", result["email"]])
|
|
if "qslmgr" in result:
|
|
t.add_row(["QSL via:", result["qslmgr"]])
|
|
t.align = "l"
|
|
t.header = False
|
|
return t
|
|
|
|
|
|
# Print a pretty ascii table containing all
|
|
# previously entered user data
|
|
def getQSODetailTable(qso):
|
|
t = PrettyTable(['key', 'value'])
|
|
for q in qso:
|
|
t.add_row([qso[q][0], qso[q][1]])
|
|
t.align = "l"
|
|
t.header = False
|
|
return t
|
|
|
|
|
|
# Queries QSO specific data from the user via
|
|
# the command line
|
|
def queryQSOData(qso):
|
|
dt = datetime.datetime.now(timezone.utc)
|
|
dt_now = dt.replace(tzinfo=timezone.utc)
|
|
|
|
# pre-fill the fields with date, time and
|
|
# default values from the config file
|
|
qso_date = dt_now.strftime("%Y%m%d")
|
|
time_on = dt_now.strftime("%H%M")
|
|
band = config['qrzlogger']['band']
|
|
mode = config['qrzlogger']['mode']
|
|
rst_rcvd = config['qrzlogger']['rst_rcvd']
|
|
rst_sent = config['qrzlogger']['rst_sent']
|
|
tx_pwr = config['qrzlogger']['tx_pwr']
|
|
comment = ""
|
|
|
|
# If this is the first try filling out the QSO fields
|
|
# then we use defaults
|
|
if qso is None:
|
|
questions = {
|
|
"qso_date" : ["QSO Date",qso_date],
|
|
"time_on": ["QSO Time", time_on],
|
|
"band": ["Band", band],
|
|
"mode": ["Mode", mode],
|
|
"rst_rcvd": ["RST Received", rst_rcvd],
|
|
"rst_sent": ["RST Sent", rst_sent],
|
|
"tx_pwr": ["Power (in W)", tx_pwr],
|
|
"comment": ["Comment", comment]
|
|
}
|
|
# if this is not the first try, we pre-fill the
|
|
# vaulues we got from the last try
|
|
else:
|
|
questions = qso
|
|
|
|
# We now loop through all defined fields and ask
|
|
# the user for input
|
|
for q in questions:
|
|
txt = inputcol + questions[q][0] + " [" + defvalcol + questions[q][1] + inputcol + "]:" + style.RESET
|
|
inp = input(txt)
|
|
# If the user just hits enter, we keep the default value.
|
|
# If not, we keep the data provided by the user
|
|
if inp == "c":
|
|
return None
|
|
if inp != "":
|
|
questions[q][1] = inp
|
|
|
|
return questions
|
|
|
|
|
|
# Sends the previously collected QSO information as a new
|
|
# QRZ.com logbook entry via the API
|
|
def sendQSO(qso):
|
|
is_ok = False
|
|
|
|
# construct ADIF QSO entry
|
|
adif = '<station_callsign:' + str(len(config['qrzlogger']['station_call'])) + '>' + config['qrzlogger']['station_call']
|
|
adif += '<call:' + str(len(call)) + '>' + call
|
|
for field in qso:
|
|
adif += '<' + field + ':' + str(len(qso[field][1])) + '>' + qso[field][1]
|
|
adif += '<eor>'
|
|
|
|
# construct POST data
|
|
post_data = { 'KEY' : config['qrzlogger']['api_key'], 'ACTION' : 'INSERT', 'ADIF' : adif }
|
|
|
|
# URL encode the payload
|
|
data = urllib.parse.urlencode(post_data)
|
|
# send the POST request to QRZ.com
|
|
resp = requests.post(config['qrzlogger']['api_url'], headers=headers, data=data)
|
|
str_resp = resp.content.decode("utf-8")
|
|
response = urllib.parse.unquote(str_resp)
|
|
# Check if the upload failed and print out
|
|
# the reason plus some additional info
|
|
if "STATUS=FAIL" in response:
|
|
print(errorcol)
|
|
print("QSO upload failed. QRZ.com has send the following reason:\n")
|
|
resp_list = response.split("&")
|
|
for item in resp_list:
|
|
print(item)
|
|
print("\nPlease review the following request that led to this error:\n")
|
|
print(style.RESET)
|
|
print(post_data)
|
|
else:
|
|
print(successcol)
|
|
print("QSO successfully uploaded to QRZ.com")
|
|
print(style.RESET)
|
|
is_ok = True
|
|
return is_ok
|
|
|
|
|
|
# ask a user a simple y/n question
|
|
# returns True if "y"
|
|
# returns False in "n"
|
|
def askUser(question):
|
|
while True:
|
|
inp = input("\n" + inputcol + question + " [" + defvalcol + "y/n" + inputcol + "]: " + style.RESET)
|
|
if inp == "y":
|
|
return True
|
|
elif inp == "n":
|
|
return False
|
|
|
|
|
|
# Main routine
|
|
if __name__ == '__main__':
|
|
|
|
keeponlogging = True
|
|
# get a session after logging into QRZ with user/pass
|
|
get_session()
|
|
|
|
# print an awesome banner
|
|
print(logocol + " _ ")
|
|
print(" __ _ _ _ __| |___ __ _ __ _ ___ _ _ ")
|
|
print(" / _` | '_|_ / / _ \/ _` / _` / -_) '_|")
|
|
print(" \__, |_| /__|_\___/\__, \__, \___|_| ")
|
|
print(" |_| |___/|___/ " + style.RESET)
|
|
|
|
# Begin the main loop
|
|
while keeponlogging:
|
|
# query a call sign from the user
|
|
call = input("\n\n%sEnter Callsign:%s " % (inputcol, style.RESET))
|
|
# check if it has the format of a valid call sign
|
|
# (at least 3 characters, only alphanumeric and slashes)
|
|
if len(call) > 2 and call.replace("/", "").isalnum():
|
|
# make the call sign all upper case
|
|
call = call.upper()
|
|
# query call sign data from QRZ
|
|
result = getCallData(call)
|
|
# the query was successful
|
|
if result:
|
|
print ('\n%s%sQRZ.com results for %s%s' % (style.UNDERLINED, hlcol, call, style.RESET))
|
|
# generate a nice ascii table with the result
|
|
tab = getXMLQueryTable(result)
|
|
# print the table
|
|
print(tablecol)
|
|
print(tab)
|
|
print(style.RESET)
|
|
# the query was unsuccessful
|
|
else:
|
|
print ('\n%s%s has no record on QRZ.com ¯\_(ツ)_/¯%s' % (errorcol, call, style.RESET))
|
|
# ask the user if he/she likes to continue anyway
|
|
if not askUser("Continue with logging this QSO?"):
|
|
# restart from the beginning
|
|
break
|
|
print("")
|
|
# pull all previous QSOs from tzhe QRZ logbook
|
|
result = getQSOsForCallsign(call)
|
|
# ignore this part if there were no previous QSOs
|
|
if result[0]:
|
|
print ('%s%sPrevious QSOs with %s%s' % (style.UNDERLINED, hlcol, call, style.RESET))
|
|
# generate a nice ascii table with the result
|
|
tab = getQSOTable(result)
|
|
# print the table
|
|
print(tablecol)
|
|
print(tab)
|
|
print(style.RESET)
|
|
|
|
print ('%s%sEnter new QSO details below%s%s (enter \'c\' to cancel)%s\n' % (style.UNDERLINED, hlcol, style.RESET, hlcol, style.RESET,))
|
|
|
|
qso_ok = False
|
|
qso = None
|
|
ask_try_again = False
|
|
|
|
# we now ask the user for QSO details until he/she is happy with the result
|
|
while not qso_ok:
|
|
# query QSO details from the user
|
|
qso = queryQSOData(qso)
|
|
# the user has answered all questions
|
|
if qso:
|
|
print ('\n%s%sPlease review your choices%s' % (style.UNDERLINED, hlcol, style.RESET))
|
|
# generate a pretty table
|
|
tab = getQSODetailTable(qso)
|
|
# print the table
|
|
print(tablecol)
|
|
print(tab)
|
|
print(style.RESET)
|
|
# ask user if everything is ok. If not, start over.
|
|
if askUser("Is this correct?"):
|
|
qso_ok = sendQSO(qso)
|
|
# QSO successfully sent.
|
|
if qso_ok:
|
|
qso = None
|
|
keeponlogging = askUser("Log another QSO?")
|
|
# QSO upload failed
|
|
else:
|
|
ask_try_again = True
|
|
else:
|
|
ask_try_again = True
|
|
# We ask the user if he/she wants to try again
|
|
# and - if not - another QSO should be logged
|
|
if ask_try_again:
|
|
if not askUser("Try again?"):
|
|
# user answered with "n"
|
|
# we quit the loop and reset the QSO fields
|
|
qso_ok = True
|
|
qso = None
|
|
if not askUser("Log another QSO?"):
|
|
# quit the application
|
|
keeponlogging = False
|
|
# the user has entered 'c' during the QSO detail entering process
|
|
else:
|
|
qso_ok = True
|
|
qso = None
|
|
# the user entered a too short callsign or invalid characters
|
|
else:
|
|
print(errorcol + "\nPlease enter a callsign with\n * at least 3 characters\n * only letters, numbers and slashes" + style.RESET)
|
|
|
|
|
|
print(inputcol)
|
|
print("73!")
|
|
print(style.RESET)
|