FastLBRY-GTK-master/flbry/analytics.py
auser e2cdd0e5fc Add commit 6a34355fda files minus lbrynet binary
Add files from 6a34355fda commit of FastLBRY-GTK notabug.org repo
by jyamihud without the flbry/lbrynet binary
2026-09-29 20:07:22 +00:00

830 lines
23 KiB
Python

#####################################################################
# #
# THIS IS A SOURCE CODE FILE FROM A PROGRAM TO INTERACT WITH THE #
# LBRY PROTOCOL ( lbry.com ). IT WILL USE THE LBRY SDK ( lbrynet ) #
# FROM THEIR REPOSITORY ( https://github.com/lbryio/lbry-sdk ) #
# WHICH I GONNA PRESENT TO YOU AS A BINARY. SINCE I DID NOT DEVELOP #
# IT AND I'M LAZY TO INTEGRATE IN A MORE SMART WAY. THE SOURCE CODE #
# OF THE SDK IS AVAILABLE IN THE REPOSITORY MENTIONED ABOVE. #
# #
# ALL THE CODE IN THIS REPOSITORY INCLUDING THIS FILE IS #
# (C) J.Y.Amihud and Other Contributors 2021. EXCEPT THE LBRY SDK. #
# YOU CAN USE THIS FILE AND ANY OTHER FILE IN THIS REPOSITORY UNDER #
# THE TERMS OF GNU GENERAL PUBLIC LICENSE VERSION 3 OR ANY LATER #
# VERSION. TO FIND THE FULL TEXT OF THE LICENSE GO TO THE GNU.ORG #
# WEBSITE AT ( https://www.gnu.org/licenses/gpl-3.0.html ). #
# #
# THE LBRY SDK IS UNFORTUNATELY UNDER THE MIT LICENSE. IF YOU ARE #
# NOT INTENDING TO USE MY CODE AND JUST THE SDK. YOU CAN FIND IT ON #
# THEIR OFFICIAL REPOSITORY ABOVE. THEIR LICENSE CHOICE DOES NOT #
# SPREAD ONTO THIS PROJECT. DON'T GET A FALSE ASSUMPTION THAT SINCE #
# THEY USE A PUSH-OVER LICENSE, I GONNA DO THE SAME. I'M NOT. #
# #
# THE LICENSE CHOSEN FOR THIS PROJECT WILL PROTECT THE 4 ESSENTIAL #
# FREEDOMS OF THE USER FURTHER, BY NOT ALLOWING ANY WHO TO CHANGE #
# THE LICENSE AT WILL. SO NO PROPRIETARY SOFTWARE DEVELOPER COULD #
# TAKE THIS CODE AND MAKE THEIR USER-SUBJUGATING SOFTWARE FROM IT. #
# #
#####################################################################
# Since LBRY wallet is just a long stream of increasing and decreasing
# numbers. I call this the analytics.
import os
import math
import json
import time
import random
import colorsys
import threading
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import cairo
from flbry import fetch
from flbry import settings
def make_colors(amount=4, color=(0.1,0.5,0.8)):
# TODO: Figure out how to got the themes prefered active color
# and use it here.
colors = []
for i in range(amount):
nc = colorsys.rgb_to_hsv(*color)
nc = list(nc)
if i != 0:
nc[1] = 0
nc[2] = (i)*(1/amount)
colors.append(colorsys.hsv_to_rgb(*nc))
return colors
def window(win):
awin = Gtk.Window()
awin.set_title("FastLBRY GTK: Wallet / Analytics")
awin.set_size_request(500, 500)
notebook = Gtk.Notebook()
notebook.set_scrollable(True)
awin.add(notebook)
############################ ANALYTICS SECTION #############################
box = Gtk.VBox()
notebook.append_page(box, Gtk.Label("Totals / Analytics"))
# Let's get...
raw_balance = fetch.lbrynet("wallet_balance")
# ... and format the balance information.
balance = {
"Spendable":raw_balance["available"],
"Claim Bids":raw_balance["reserved_subtotals"]["claims"],
"Supports":raw_balance["reserved_subtotals"]["supports"],
"Tips":raw_balance["reserved_subtotals"]["tips"],
}
# Pie chart for balance
def conv(t):
return str(t) + " LBC"
box.pack_start(pie_chart(win, balance, "Totals", converter=conv), 0, 0 , 0)
minus_30_days = 60*60*24*30*-1
transactions = {"items":[], # The list of items
"zoom":[0,0], #minus_30_days,0], # The range of the selection ( by timestamp )
"allow_negative":True
}
def getting_all_transactions(data, win):
# This is a small function funnin in a separate thread, loading
# all transactions so the graph could draw them.
# TODO: this commented code works perfectly fine to get the kind of
# data that I want. But it's hell'a heavy on my computer. Last time
# I tried, it froze. So I need to figure out something about this.
# cash_file = settings.get_settings_folder()+"GTK-transactions-graph.json"
# try:
# with open(cash_file) as json_file:
# data["items"] = json.load(json_file)
# except Exception as e:
# print(e)
# def add(out, addpage):
# ret = False
# added = 0
# count = 0
# for i in out["items"]:
# # Removing unnesesary data
# a = i.copy()
# # a["amount"] = i["amount"]
# # a["timestamp"] = i["timestamp"]
# if a["timestamp"] == None: # Too recent
# a["timestamp"] = int(time.time())
# # a["txid"] = i["txid"]
# # a["name"] = i.get("name", "")
# if a not in data["items"]:
# added += 1
# data["items"].append(a)
# elif not addpage:
# count += 1
# ret = True
# return ret
# Fetching
# out = fetch.lbrynet("transaction_list", { "page_size":50})
# all_of_them = out["total_items"]
# addpage = 0
# add(out, 0)
# for i in range(out["total_pages"]):
# out = fetch.lbrynet("transaction_list", {"page":addpage+i+2,
# #"exclude_internal_transfers": True,
# #"no_totals":True,
# "page_size":50})
# if add(out, addpage):
# addpage = int((len(data["items"]))/50) - 2
# # Save to cash
# with open(cash_file, 'w') as fp:
# json.dump(data["items"], fp)
# if not win.keep_loading_the_wallet_graph or all_of_them == len(data["items"]):
# return
# THE MEANWHILE SOLUTION
out = fetch.lbrynet("txo_plot", { "days_back":1000, # Fetch 100 days of txo
"exclude_internal_transfers":True, # Without crap
"is_not_my_input":True, # Not from me ( as in support only )
})
for i in out:
a = {}
a["amount"] = i["total"]
a["timestamp"] = int(time.mktime(time.strptime(i["day"],"%Y-%m-%d")))
data["items"].append(a)
win.keep_loading_the_wallet_graph = True
t = threading.Thread(target=getting_all_transactions, args=(transactions, win))
t.setDaemon(True)
t.start()
def kill_graph(w):
win.keep_loading_the_wallet_graph = False
awin.connect("destroy", kill_graph)
# Graph with the history of all transactions
the_graph = graph(win, transactions, "Totals")
box.pack_start(the_graph, 1, 1 , 0)
awin.show_all()
def box_of_color(color):
def on_draw(widget, cr, data):
width = widget.get_allocated_width()
height = widget.get_allocated_height()
cr.set_source_rgb(*color)
cr.rectangle(0, 0, width, height)
cr.fill()
area = Gtk.DrawingArea()
area.set_size_request(40, 40)
area.connect("draw", on_draw, color)
return area
def pie_chart_draw(d, main_layer, win, data, colors):
# Need to know how big is our chart
w = d.get_allocated_width()
h = d.get_allocated_height()
# We want our circle to fit, so we find which one is
# smaller.
smaller = min(w, h)
last = 0
whole = float(math.pi*2)
sum_of_data = 0
for i in data:
sum_of_data += float(data[i])
for n, i in enumerate(data):
this = whole* ( float(data[i]) / sum_of_data )
main_layer.move_to(w/2, h/2)
main_layer.arc(w/2, h/2, smaller/2, last, last+this)
main_layer.close_path()
main_layer.set_source_rgb(*colors[n%len(colors)])
main_layer.fill()
last = last+this
def pie_chart(win, data, title="", converter=False):
ret = Gtk.HBox(True)
colors = make_colors(len(data))
da = Gtk.DrawingArea()
da.connect("draw", pie_chart_draw, win, data, colors)
ret.pack_start(da, 1,1,5)
lbox = Gtk.VBox()
ret.pack_start(lbox, 1,1,5)
sum_of_data = 0
for i in data:
sum_of_data += float(data[i])
if converter:
sum_of_data = converter(sum_of_data)
lbox.pack_start(Gtk.Label(" Total : "+str(sum_of_data)+" "), 0,0,1)
for n, i in enumerate(data):
ibox = Gtk.HBox()
lbox.pack_start(ibox, 0,0,3)
ibox.pack_start(box_of_color(colors[n%len(colors)]), 0,0,0)
show_size = data[i]
if converter:
show_size = converter(show_size)
ibox.pack_start(Gtk.Label(" "+i+": "+str(show_size)+" "), 0,0,1)
return ret
def graph_draw(d, main_layer, win, data, currancy, graph_addition):
data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
# Need to know how big is our graph
w = d.get_allocated_width()
h = d.get_allocated_height()
if data.get("allow_negative", False):
zero_at = h / 2
else:
zero_at = h
# The mouse position of a given frame
mx = d.get_pointer()[0]
my = d.get_pointer()[1]
# Test of the mouse position
# main_layer.move_to(0,0)
# main_layer.line_to(mx, my)
# main_layer.stroke()
len_day = 60*60*24 # Seconds in a day
len_hour = 60*60 # Seconds in an hour
len_minute = 60 # Seconds in a minute
# Here we are getting the latest and the earliest
# timestamp, so we could calculate the step of the
# graph. ( So the data will be readable )
latest = 0
try:
earliest = data["items"][0]["timestamp"]
except:
earliest = 0
for i in data["items"]:
if i.get("timestamp", 0) > latest:
latest = i.get("timestamp", 0)
if i.get("timestamp", 0) < earliest:
earliest = i.get("timestamp", 0)
# Now let's look at our zoom value
to_zoom = data["zoom"][0] != earliest or data["zoom"][1] != latest
if data["zoom"][0] < earliest or data["zoom"][0] == 0:
data["zoom"][0] = earliest
if data["zoom"][1] > latest or data["zoom"][1] == 0:
data["zoom"][1] = latest
earliest, latest = data["zoom"]
# Now I want to make a scale of dates from left
# to right.
main_layer.select_font_face("Monospace")
main_layer.set_font_size(10)
full_date = "%Y-%m-%d %H:%M:%S"
only_date = "%Y-%m-%d"
if latest - earliest > 10 * len_day:
show_format = only_date
count = int( w / (len("xxxx-xx-xx")*6+12) )
else:
show_format = full_date
count = int( w / (len("xxxx-xx-xx xx:xx:xx")*6+12) )
# Now I want to show the current date / time for
# the area where the user is hovering.
suglen = len("xxxx-xx-xx xx:xx:xx")*6+12
thexm = mx-suglen/2
if thexm < 2:
thexm = 2
elif thexm > w - suglen - 2:
thexm = w - suglen - 2
try:
res_date = int( ( latest - earliest ) / w * mx + earliest )
show_date = time.strftime(full_date, time.gmtime(res_date))
except:
show_date = "0000-00-00"
main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
main_layer.rectangle(2+thexm,2,len(show_date)*6+2, 14)
main_layer.fill()
main_layer.move_to(3+thexm,12)
main_layer.set_source_rgba(1,1,1,1)
main_layer.show_text(str(show_date))
# main_layer.set_source_rgba(0.7,0.7,0.7,1)
# main_layer.move_to( mx, 20 )
# main_layer.line_to( mx, h )
# main_layer.stroke()
# main_layer.set_dash([10,10])
# main_layer.set_source_rgba(0.2,0.2,0.2,1)
# main_layer.move_to( mx, 20 )
# main_layer.line_to( mx, h )
# main_layer.stroke()
# main_layer.set_dash([1])
# And the rest of the dates
for date in range(count):
try:
res_date = int( ( latest - earliest ) / count * date + earliest )
show_date = time.strftime(show_format, time.gmtime(res_date))
except:
show_date = "0000-00-00"
thex = w / count * date
# If not in range of the mouse ( so I could show the current day
# for that specific area ).
if int(thex) not in range(int(thexm-suglen/2), int(thexm+suglen)):
main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
main_layer.rectangle(2+thex,2,len(show_date)*6+2, 14)
main_layer.fill()
main_layer.move_to(3+thex,12)
main_layer.set_source_rgba(1,1,1,1)
main_layer.show_text(str(show_date))
# A step is how often will there be a data point
# of the graph. Step of one minute, means every
# point on the graph will consist all the data
# happened in this minute.
step = (latest - earliest) / (w / 2) # A second
# Now we need the smallest and biggest value in a
# given step
values = []
times = []
pstep = earliest
s = 0
av = []
for n, i in enumerate(data["items"]):
if i.get("timestamp", 0) < earliest:
continue
if graph_addition == "add":
s += float(i.get("amount", i.get("value", 0)))
elif graph_addition == "average":
av.append( float(i.get("amount", i.get("value", 0))) )
elif graph_addition == "last":
s = float(i.get("amount", i.get("value", 0)))
if i.get("timestamp", 0) > pstep + step-1:
pstep = i.get("timestamp", n)
if graph_addition == "average":
try:
values.append(sum(av)/len(av))
except:
values.append(0)
else:
values.append(s)
times.append(pstep)
s = 0
av = []
if i.get("timestamp", 0) > latest:
break
# Finding the farthest point from the center
# center being the 0 (zero)
try:
biggest = max(values)
if min(values) * -1 > biggest:
biggest = min(values) * -1 # Multuply by -1 reverses the - to a +
except Exception as e:
biggest = 1
# Now let's draw it
main_layer.set_line_cap(cairo.LineCap.ROUND)
# POSITIVE VALUE
try:
toy = ( zero_at ) - ( ( zero_at ) / biggest * values[0] ) *0.9
except:
toy = zero_at
#toy = min(toy, zero_at)
main_layer.rectangle(0,0,w,zero_at)
main_layer.clip()
main_layer.move_to(0, toy)
prex = 0
prey = toy
toxes = []
toyes = []
for n, i in enumerate(values):
tox = w / (latest - earliest) * (times[n]-earliest)
try:
toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
except:
toy = zero_at
toxes.append(tox)
toyes.append(toy)
#toy = min(toy, zero_at)
main_layer.curve_to(
tox - (tox - prex)/2,
prey,
prex + (tox - prex)/2,
toy,
tox,
toy)
prex = tox
prey = toy
main_layer.line_to( w, zero_at)
main_layer.line_to( 0, zero_at)
main_layer.set_source_rgba(0.2,0.8,0.2,0.5)
main_layer.fill_preserve()
main_layer.set_source_rgba(0.2,0.8,0.2,1)
main_layer.stroke()
# NEGATIVE VALUE
try:
toy = ( zero_at ) - ( ( zero_at ) / biggest * values[0] ) *0.9
except:
toy = zero_at
#toy = max(toy, zero_at)
main_layer.reset_clip()
main_layer.rectangle(0,zero_at,w,h)
main_layer.clip()
main_layer.move_to(0, toy)
prex = 0
prey = toy
for n, i in enumerate(values):
tox = w / (latest - earliest) * (times[n]-earliest)
try:
toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
except:
toy = zero_at
#toy = max(toy, zero_at)
main_layer.curve_to(
tox - (tox - prex)/2,
prey,
prex + (tox - prex)/2,
toy,
tox,
toy)
prex = tox
prey = toy
main_layer.line_to( w, zero_at)
main_layer.line_to( 0, zero_at)
main_layer.set_source_rgba(0.8,0.2,0.2,0.5)
main_layer.fill_preserve()
main_layer.set_source_rgba(0.8,0.2,0.2,1)
main_layer.stroke()
main_layer.reset_clip()
# Reference line
main_layer.set_source_rgba(0.7,0.7,0.7,1)
main_layer.move_to( 0, zero_at )
main_layer.line_to( w, zero_at )
main_layer.stroke()
main_layer.set_dash([10,10])
main_layer.set_source_rgba(0.2,0.2,0.2,1)
main_layer.move_to( 0, zero_at )
main_layer.line_to( w, zero_at )
main_layer.stroke()
main_layer.set_dash([1])
# MOUSE OVER SELECTOR
def closest(l, v):
distances = []
for i in l:
distances.append(max(i-v, v-i))
try:
return l[distances.index(min(distances))]
except:
return 0
selectx = closest(toxes, mx)
if selectx:
selecty = toyes[toxes.index(selectx)]
# Litte circle
main_layer.arc(selectx, selecty, 8, 0, math.pi*2)
main_layer.set_source_rgba(0.2,0.8,0.2,1)
if selecty > zero_at:
main_layer.set_source_rgba(0.8,0.2,0.2,1)
main_layer.fill()
# Line from that circle downwards
main_layer.move_to(selectx, selecty)
main_layer.line_to(selectx, zero_at)
main_layer.stroke()
# Data about this time frame
to_data = times[toxes.index(selectx)]
from_data = to_data - step
try:
from_data = time.strftime(show_format, time.gmtime(from_data))
except:
from_data = "0000-00-00"
try:
to_data = time.strftime(show_format, time.gmtime(to_data))
except:
to_data = "0000-00-00"
# Counting the largest thing
plist = ["From: "+from_data,
"To: "+to_data,
"Total: "+currancy+" "+str(round(values[toxes.index(selectx)], 2)) ]
leng = 0
for thing in plist:
if len(str(thing))*6+2 > leng:
leng = len(str(thing))*6+2
if selectx > w/2:
recx = selectx - leng - 10
else:
recx = selectx + 10
if selecty + len(plist)*15 > h:
recy = selecty - len(plist)*15
else:
recy = selecty
main_layer.set_source_rgba(0.1,0.1,0.1,0.7)
main_layer.rectangle(recx, recy, leng, len(plist)*15)
main_layer.fill()
for n, thing in enumerate(plist):
main_layer.move_to(recx+2, recy+12+(15*n))
main_layer.set_source_rgba(1,1,1,1)
main_layer.show_text(thing)
# Now let's get the values ( to the side of the graph )
for i in range(int(h/20)):
# TODO: This has to be tuned a bit. It's not perfect. But it's
# very close.
they = i*20+20
try:
value_is = round( biggest / zero_at * (zero_at - they), 2)
except Exception as e:
print("what", e)
value_is = 0
show_value = currancy + " " + str(value_is)
if mx > w / 2:
main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
main_layer.rectangle(2, 2+they,len(show_value)*6+4, 14)
main_layer.fill()
main_layer.move_to(3,12+they)
main_layer.set_source_rgba(1,1,1,1)
main_layer.show_text(show_value)
else:
main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
main_layer.rectangle(w-len(show_value)*6-4, 2+they,len(show_value)*6+4, 14)
main_layer.fill()
main_layer.move_to(w-len(show_value)*6-3,12+they)
main_layer.set_source_rgba(1,1,1,1)
main_layer.show_text(show_value)
# Render a little pressed selector
if "pressed" in data:
for i in [data["pressed"], mx]:
main_layer.set_source_rgba(0.7,0.7,0.7,1)
main_layer.move_to( i, 0 )
main_layer.line_to( i, h )
main_layer.stroke()
main_layer.set_dash([10,10])
main_layer.set_source_rgba(0.2,0.2,0.2,1)
main_layer.move_to( i, 0 )
main_layer.line_to( i, h )
main_layer.stroke()
main_layer.set_dash([1])
# Keep redrawing the graph
d.queue_draw()
def graph_button_press(w, e, data, da):
data["pressed"] = e.x
print(data["zoom"])
print(e.x, e.y)
def graph_button_release(w, e, data, da):
if "pressed" in data:
x = data["pressed"]
# If there was no motion
if x-2 < e.x < x+2:
data["zoom"] = [0,0]
else:
w = da.get_allocated_width()
zoom0 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * min(x, e.x))
zoom1 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * max(x, e.x))
data["zoom"] = [zoom0, zoom1]
print(data["zoom"])
del data["pressed"]
def graph(win, data, title="", currancy="$", add_now=True, add_value="Same", graph_addition="add"):
# adding one more data point for "now"
if add_now:
try:
data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
last = data["items"][-1].copy()
last["timestamp"] = int(time.time())
if not add_value == "Same":
last["amount"] = add_value
data["items"].append(last)
except:
pass
event_box = Gtk.EventBox()
da = Gtk.DrawingArea()
da.set_size_request(100,100)
da.connect("draw", graph_draw, win, data, currancy, graph_addition)
event_box.connect("button-press-event", graph_button_press, data, da)
event_box.connect("button-release-event", graph_button_release, data, da)
event_box.add(da)
return event_box