From 3c96c7d1166f06d9af2c0f9a576bd10413aa2c90 Mon Sep 17 00:00:00 2001 From: pronopython Date: Mon, 22 Jul 2024 11:05:11 +0200 Subject: [PATCH] History of visited spots with ability to go back and forth added; reformatted with black --- README.md | 38 ++- rugivi/config_file_handler.py | 20 +- .../organic_crawler_first_edition.py | 121 ++++----- rugivi/dialogs.py | 7 +- rugivi/dir_helper.py | 6 +- rugivi/exports/world_overlook.py | 9 +- rugivi/fap_table/fap_table.py | 3 +- rugivi/fap_table/fap_table_manager.py | 20 +- rugivi/fap_table/fap_table_view.py | 7 +- rugivi/fap_table/fap_tables.py | 5 +- .../image_server_database.py | 14 +- .../abstract_cache_filename_generator.py | 5 +- rugivi/image_service/image_cache.py | 24 +- rugivi/image_service/image_server.py | 32 +-- rugivi/image_service/video_still_generator.py | 176 ++----------- rugivi/landmarks/landmark_service.py | 51 ++++ rugivi/landmarks/point_of_interest.py | 57 ++++ rugivi/landmarks/tour.py | 136 ++++++++++ rugivi/print_module_dir.py | 1 + rugivi/rugivi.py | 246 ++++++++++++------ rugivi/rugivi_configurator.py | 59 +++-- rugivi/rugivi_image_cache_maintenance.py | 81 +++--- rugivi/selection.py | 24 +- rugivi/status.py | 35 +-- rugivi/thread_safe_list.py | 2 +- rugivi/view.py | 61 +++-- .../abstract_world_database.py | 11 +- .../world_database_service/world_database.py | 6 +- rugivi/world_things/abstract_world.py | 11 +- rugivi/world_things/chunk.py | 10 +- rugivi/world_things/frame.py | 10 +- rugivi/world_things/world.py | 8 +- setup.py | 58 +++-- 33 files changed, 795 insertions(+), 559 deletions(-) create mode 100644 rugivi/landmarks/landmark_service.py create mode 100644 rugivi/landmarks/point_of_interest.py create mode 100644 rugivi/landmarks/tour.py diff --git a/README.md b/README.md index 68b7b88..b67d4ab 100755 --- a/README.md +++ b/README.md @@ -223,6 +223,8 @@ You can watch this process or start traveling through the images world. |i |Toggle grey information box | |1-7 |Zoom levels | |0 |Zoom fit | +|b |Go backward in history to previous selection | +|f |Go forward in history to next selection | |j |Jump randomly | |n |Open image with system image viewer; Open video with system video player of VLC | |t |Call [Fapel-System](https://github.com/pronopython/fapel-system) Tagger on selection| @@ -230,7 +232,7 @@ You can watch this process or start traveling through the images world. |up / down |Open and go through Fapel Table rows | |left / right |Go through Fapel Tables of one row | |g |Open Dialog to go to a spot by coordinates x,y | -|c |Open information dialog | +|c |Open information dialog | |o |Pause image server (troubleshooting) | |p |Pause crawler (troubleshooting) | |e |Generate and export world map as png file | @@ -706,14 +708,46 @@ in `rugivi.conf`, RuGiVi will not load images but rather represent every directo # 📢 Community Support +Please report errors you find! For example, I did not find out that the rugivi_configurator could not select new files for about 4 releases. I sure cannot test every setup or every way you could use RuGiVi 😮 so your input is **gold** for me! + +Since you may not like to be connected to be using RuGiVi, you can contact me **one-way anonymously** and report errors! + +## Report errors and send ideas anonymously + +Head over to + +[my bug report at formape.com](https://www.formape.com/f/phqrw) + +and fill out the bug report form. You do not need an account or provide any form of personal information (mail address etc). Just help me find errors and improve RuGiVi! + +You can also contact me via email: pronopython@proton.me . If you want to contact me anonymously, create yourself a burner email account. + +## Report errors via GitHub The [GitHub discussion boards](https://github.com/pronopython/rugivi/discussions) are open for sharing ideas and plans for RuGiVi. You can report errors through a [GitHub issue](https://github.com/pronopython/rugivi/issues/new). -Don't want to use GitHub? You can also contact me via email: pronopython@proton.me If you want to contact me anonymously, create yourself a burner email account. + # Release Notes +## v0.6.0-alpha + +### added + +- History of visited spots with ability to go back and forth added + +### fixed + +- rugivi_configurator now uses save-as dialog (selecting a non existing new sqlite file is now possible) +- selection left the visible area when screen was moved and switched back to screen to late +- jump did not change selection when target spot was visible on screen (only the screen was centered) + +### changed + +- Reformatted all files with black +- Removed dead or commented out code (using flake8) + ## v0.5.2-alpha ### fixed diff --git a/rugivi/config_file_handler.py b/rugivi/config_file_handler.py index 66c5cf3..71c96c0 100755 --- a/rugivi/config_file_handler.py +++ b/rugivi/config_file_handler.py @@ -30,7 +30,6 @@ # import os -from pathlib import Path import configparser import pathlib import sys @@ -46,8 +45,10 @@ def __init__(self, configFilePath, create_if_missing=False) -> None: self.config_file_path = dir_helper.expand_home_dir(self.config_file_path) if create_if_missing and not os.path.isfile(self.config_file_path): - pathlib.Path(os.path.dirname(self.config_file_path)).mkdir(parents=True, exist_ok=True) - open(self.config_file_path, 'a').close() + pathlib.Path(os.path.dirname(self.config_file_path)).mkdir( + parents=True, exist_ok=True + ) + open(self.config_file_path, "a").close() self.create_config_parser_and_load_config() self.config_changed = False @@ -101,9 +102,16 @@ def check_key(self, group, key): try: self.config_parser.get(group, key) except configparser.NoOptionError: - errortext = 'Error: The following group / key combination is missing in your RuGiVi config "'+self.config_file_path+'":\n\n['+group+']\n'+key+'\n\nLook into rugivi dir of git repo for a default config file!' + errortext = ( + 'Error: The following group / key combination is missing in your RuGiVi config "' + + self.config_file_path + + '":\n\n[' + + group + + "]\n" + + key + + "\n\nLook into rugivi dir of git repo for a default config file!" + ) print("Config entry missing!") print(errortext) - messagebox.showerror('Config entry missing!', errortext) + messagebox.showerror("Config entry missing!", errortext) sys.exit() - diff --git a/rugivi/crawlers/first_organic/organic_crawler_first_edition.py b/rugivi/crawlers/first_organic/organic_crawler_first_edition.py index e724c58..aa3a18e 100644 --- a/rugivi/crawlers/first_organic/organic_crawler_first_edition.py +++ b/rugivi/crawlers/first_organic/organic_crawler_first_edition.py @@ -30,22 +30,12 @@ # import math -from rugivi.image_service.image_server import ( - Frame, - ImageServer, - StreamedImage, - World, - sleep, - threading, -) +from rugivi.image_service.image_server import ImageServer from rugivi.image_service.streamed_image import StreamedImage from rugivi.world_database_service.world_database import ( WorldDatabase, ) -from rugivi.world_things.frame import Frame from rugivi.world_things.world import Frame, World - - import os import random import threading @@ -73,10 +63,10 @@ def __init__( crawl_videos=True, excludeDirList=[], mockup_mode=True, - cross_shape_grow = False, - no_diagonal_grow = True, - organic_grow = True, - reach_out_ant_mode = True, + cross_shape_grow=False, + no_diagonal_grow=True, + organic_grow=True, + reach_out_ant_mode=True, ) -> None: self.db = WorldDatabase(crawler_db_file_path, world, image_server) @@ -90,7 +80,7 @@ def __init__( self.basedir: str = self.db.get_and_put_if_none( WorldDatabase.KEY_BASEDIR, basedir ) - self.excludeDirList=excludeDirList + self.excludeDirList = excludeDirList self.mockup_mode = mockup_mode @@ -195,7 +185,9 @@ def crawler_loop(self) -> None: current_dir_absolute_path = Path(current_dir).absolute() - if os.path.basename(os.path.normpath(current_dir_absolute_path)).startswith("."): + if os.path.basename(os.path.normpath(current_dir_absolute_path)).startswith( + "." + ): # hidden dir (started with ".") self.excludeDirList.append(str(current_dir_absolute_path)) continue @@ -207,9 +199,8 @@ def crawler_loop(self) -> None: skip_dir = False for exdir in self.excludeDirList: - #print(current_dir_absolute_path," ?= ",exdir) if str(current_dir_absolute_path).startswith(exdir): - print("Excluded dir",current_dir_absolute_path,"is skipped") + print("Excluded dir", current_dir_absolute_path, "is skipped") skip_dir = True break if skip_dir: @@ -268,7 +259,9 @@ def crawler_loop(self) -> None: file_path, StreamedImage.QUALITY_THUMB ) else: - streamed_image = self.imageServer.create_streamed_mockup(file_path, color_string=self.current_dir) + streamed_image = self.imageServer.create_streamed_mockup( + file_path, color_string=self.current_dir + ) (start_spot_x_S, start_spot_y_S) = found_empty_spots[file_number] @@ -362,7 +355,9 @@ def crawler_loop(self) -> None: file_path, StreamedImage.QUALITY_THUMB ) else: - streamed_image = self.imageServer.create_streamed_mockup(file_path, color_string=video_file_path) + streamed_image = self.imageServer.create_streamed_mockup( + file_path, color_string=video_file_path + ) streamed_image.set_extended_attribute("video_still_uuid", uuid_name) streamed_image.set_extended_attribute("video_file", video_file_path) @@ -386,7 +381,6 @@ def crawler_loop(self) -> None: video_file_path = Path(video_file_path).absolute() self.dir_and_start_spot[str(video_file_path)] = video_start_spot - if start_spot != None: ############################################################################## @@ -447,29 +441,10 @@ def crawler_loop(self) -> None: print(self.status) def __save_to_db(self, borderSpots, dirAndStartSpot): - print("saving to db") self.status = "saving to db" self.db.put(WorldDatabase.KEY_BORDERSPOTS, borderSpots) self.db.put(WorldDatabase.KEY_DIRANDSTARTSPOT, dirAndStartSpot) - - """ - allChunks = self.world.get_all_chunks_in_memory_as_list() - for chunk in allChunks: - if chunk.is_empty(): - continue - cso = ChunkSaveObject() - cso.from_chunk(chunk) - k = str((chunk.x_C, chunk.y_C)) - # print("inserting chunk",k) - # storing in same as all the other keys is so hacky... - # but commits are per table sadly... - # and if program crashes between two commits... - self.db.db[str((chunk.x_C, chunk.y_C))] = cso.to_tuple() - - self.db.commit_db() - """ - self.db.save_world_to_database() def __pause_for_image_queue(self): @@ -508,8 +483,6 @@ def __find_empty_spots(self, current_dir, neededSpots): # fallback: fill start_spot no matter what will happen start_spot = random.choice(tuple(self.border_spots)) - #print("Trying start spot",start_spot) - if ( self.start_spot_search_method == OrganicCrawlerFirstEdition.START_SPOT_SEARCH_NEAR_PARENT @@ -520,8 +493,6 @@ def __find_empty_spots(self, current_dir, neededSpots): basedir_path = Path(current_dir) basedir = Path(self.basedir).absolute() - # error: 2 times going to parent dir! - # parentPath = path.parent.absolute() basedir_parent_path = basedir_path.absolute() # Going down the parent line while len(str(basedir_parent_path)) > len(str(basedir)): @@ -532,29 +503,35 @@ def __find_empty_spots(self, current_dir, neededSpots): parent_spot = self.dir_and_start_spot[str(basedir_parent_path)] (parent_spot_x_S, parent_spot_y_S) = parent_spot if len(list_of_unsuccessful_border_spots) < 14: - + # try x times to find an empty border spot closer and closer to the parent spot for current_round in range( - 0, 30 - len(list_of_unsuccessful_border_spots) # the more unsuccessful the search is, the less nearer the spot is selected - ): # was 300 -> more time needed for that (bad), but 30 is inaccurate + 0, + 30 + - len( + list_of_unsuccessful_border_spots + ), # the more unsuccessful the search is, the less nearer the spot is selected + ): # was 300 -> more time needed for that (bad), but 30 is inaccurate sleep(0.0002) candidate = random.choice(tuple(self.border_spots)) - #print("border spot candidate",candidate) if candidate in list_of_unsuccessful_border_spots: - #print("Border candiate",candidate, "already checked and it is not good") continue - #print("Border candiate",candidate) (candidate_x_S, candidate_y_S) = candidate (start_spot_x_S, start_spot_y_S) = start_spot - # changed "or" to "and" if self.CROSS_SHAPE_GROW: if abs(candidate_x_S - parent_spot_x_S) < abs( start_spot_x_S - parent_spot_x_S - ) and abs(candidate_y_S - parent_spot_y_S) < abs( + ) and abs(candidate_y_S - parent_spot_y_S) < abs( start_spot_y_S - parent_spot_y_S ): start_spot = candidate - elif math.dist((candidate_x_S,candidate_y_S),(parent_spot_x_S,parent_spot_y_S)) < math.dist((start_spot_x_S,start_spot_y_S),(parent_spot_x_S,parent_spot_y_S)): + elif math.dist( + (candidate_x_S, candidate_y_S), + (parent_spot_x_S, parent_spot_y_S), + ) < math.dist( + (start_spot_x_S, start_spot_y_S), + (parent_spot_x_S, parent_spot_y_S), + ): start_spot = candidate elif ( @@ -564,7 +541,6 @@ def __find_empty_spots(self, current_dir, neededSpots): for current_round in range(0, 50): sleep(0.0002) candidate = random.choice(tuple(self.border_spots)) - #print("border spot candidate",candidate) if candidate in list_of_unsuccessful_border_spots: continue (candidate_x_S, candidate_y_S) = candidate @@ -589,17 +565,17 @@ def __find_empty_spots(self, current_dir, neededSpots): ############################################################################## # See if enough empty spots are next to the start spot ############################################################################## - self.status = "finding biome (gather empty spots [0/"+str(neededSpots)+"])" + self.status = ( + "finding biome (gather empty spots [0/" + str(neededSpots) + "])" + ) stack_with_empty_spots_to_check: list = [start_spot] # stack always holds neighbouring empty spots to be checked # if they are not that far away so that spots are grouped together # and not so far away from another - #print("checking enough empty spots") while ( len(stack_with_empty_spots_to_check) > 0 and not found_enough_empty_spots ): - #print("size of stack_with_empty_spots_to_check:",len(stack_with_empty_spots_to_check)) sleep(0.0002) if self.running == False: print("Crawler loop stopped") @@ -609,7 +585,6 @@ def __find_empty_spots(self, current_dir, neededSpots): ############################################################################## # Select one of the empty 8 (or less) neighbours of the start_spot ############################################################################## - #print("select one empty neighbour") if not self.ORGANIC_GROW: currentSpot = stack_with_empty_spots_to_check.pop(0) else: # organic grow @@ -642,7 +617,6 @@ def __find_empty_spots(self, current_dir, neededSpots): if no_of_same_result >= max_same_result: break candidate = random.choice(stack_with_empty_spots_to_check) - #print("current_round", current_round,"currentspot:",currentSpot,"candidate", candidate) if candidate == currentSpot: no_of_same_result += 1 continue @@ -650,11 +624,8 @@ def __find_empty_spots(self, current_dir, neededSpots): sleep(0.0001) (candidate_x_S, candidate_y_S) = candidate (ux, uy) = currentSpot - # if abs(cx-sx) < abs(ux-sx) or abs(cy-sy) < abs(uy-sy): - # was both "or", now "and" if reach_out: - # changed "or" to "and" if abs(candidate_x_S - start_spot_x_S) > abs( ux - start_spot_x_S ) and abs(candidate_y_S - start_spot_y_S) > abs( @@ -665,7 +636,6 @@ def __find_empty_spots(self, current_dir, neededSpots): else: no_of_same_result += 1 else: - # changed "or" to "and" if abs(candidate_x_S - start_spot_x_S) < abs( ux - start_spot_x_S ) and abs(candidate_y_S - start_spot_y_S) < abs( @@ -685,7 +655,13 @@ def __find_empty_spots(self, current_dir, neededSpots): found_enough_empty_spots = True break - self.status = "finding biome (gather empty spots ["+str(len(found_empty_spots))+"/"+str(neededSpots)+"])" + self.status = ( + "finding biome (gather empty spots [" + + str(len(found_empty_spots)) + + "/" + + str(neededSpots) + + "])" + ) # ... and now look for its neighbours (start_spot_x_S, start_spot_y_S) = currentSpot @@ -693,7 +669,6 @@ def __find_empty_spots(self, current_dir, neededSpots): ############################################################################## # Fill stack with all empty neighbours (up to 8 neighbours) ############################################################################## - #print("add all empty adjacent spots of the neighbour") for x_S in range(start_spot_x_S - 1, start_spot_x_S + 2): for y_S in range(start_spot_y_S - 1, start_spot_y_S + 2): if start_spot_x_S == x_S and start_spot_y_S == y_S: @@ -729,24 +704,18 @@ def __find_empty_spots(self, current_dir, neededSpots): # correct border spots for currentSpot in found_empty_spots: (start_spot_x_S, start_spot_y_S) = currentSpot - #sleep(0.00003) sleep(0.00002) # go through all 8 neighbours and check changes in "border status" for x_S in range(start_spot_x_S - 1, start_spot_x_S + 2): for y_S in range(start_spot_y_S - 1, start_spot_y_S + 2): - if self.world.get_frame_at_S(x_S, y_S) != None or (x_S, y_S) in found_empty_spots: + if ( + self.world.get_frame_at_S(x_S, y_S) != None + or (x_S, y_S) in found_empty_spots + ): if (x_S, y_S) in self.border_spots: - #print("removing border spot",(x_S, y_S)) self.border_spots.remove((x_S, y_S)) # no border anymore else: if (x_S, y_S) not in self.border_spots: self.border_spots.add((x_S, y_S)) # a new border spot - #for bs in self.border_spots: - # if self.world.get_frame_at_S(bs[0],bs[1]) != None: - # print("BORDER SPOTS CONTAIN NON EMPTY SPOT:",bs) - - #print("found empty spots",found_empty_spots) - #print("border spots", self.border_spots) - return start_spot, found_empty_spots diff --git a/rugivi/dialogs.py b/rugivi/dialogs.py index a8964c5..14729e5 100755 --- a/rugivi/dialogs.py +++ b/rugivi/dialogs.py @@ -31,7 +31,8 @@ import abc -from tkinter import Button, Menu, Misc, Text, Tk +from tkinter import Menu +from tkinter import Text from tkinter import Label from tkinter import Entry import tkinter.simpledialog @@ -98,7 +99,9 @@ def _show_textmenu(self, event): def bind_textmenu(self): # to be called AFTER all Entry + Text elements are placed - self.root.bind_class("Entry", "", self._show_textmenu) + self.root.bind_class( + "Entry", "", self._show_textmenu + ) self.root.bind_class("Text", "", self._show_textmenu) diff --git a/rugivi/dir_helper.py b/rugivi/dir_helper.py index db48b98..3450691 100755 --- a/rugivi/dir_helper.py +++ b/rugivi/dir_helper.py @@ -65,8 +65,12 @@ def get_config_dir(subfolder_for_windows) -> str: config_dir = os.path.join(os.environ["APPDATA"], subfolder_for_windows) return config_dir + def is_config_file_present(subfolder_for_windows, config_file_name) -> bool: - return os.path.isfile(os.path.join(get_config_dir(subfolder_for_windows),config_file_name)) + return os.path.isfile( + os.path.join(get_config_dir(subfolder_for_windows), config_file_name) + ) + def get_install_dir() -> str: return os.path.dirname(os.path.realpath(__file__)) diff --git a/rugivi/exports/world_overlook.py b/rugivi/exports/world_overlook.py index 0a2c27b..11fe802 100644 --- a/rugivi/exports/world_overlook.py +++ b/rugivi/exports/world_overlook.py @@ -32,11 +32,12 @@ import threading from PIL import Image -from time import sleep, time +from time import sleep -from ..world_things.world import * -from rugivi import dir_helper as dir_helper -from ..image_service.image_server import * +from ..world_things.world import World +from ..world_things.frame import Frame +from ..world_things.chunk import Chunk +from ..image_service.image_server import StreamedImage class WorldOverlook: diff --git a/rugivi/fap_table/fap_table.py b/rugivi/fap_table/fap_table.py index aeeec65..3c930ad 100755 --- a/rugivi/fap_table/fap_table.py +++ b/rugivi/fap_table/fap_table.py @@ -31,8 +31,7 @@ import os - -from rugivi.image_service.image_server import * +from rugivi.image_service.abstract_streamed_media import AbstractStreamedMedia class FapTableCard: diff --git a/rugivi/fap_table/fap_table_manager.py b/rugivi/fap_table/fap_table_manager.py index fa70f8c..e8adc23 100755 --- a/rugivi/fap_table/fap_table_manager.py +++ b/rugivi/fap_table/fap_table_manager.py @@ -29,11 +29,12 @@ ############################################################################################## # +import os import threading from time import sleep, time -from rugivi.thread_safe_list import * -from rugivi.fap_table.fap_table import * +from rugivi.thread_safe_list import ThreadSafeList +from rugivi.fap_table.fap_table import FapTable, FapTableCard import random from pathlib import Path import json @@ -111,8 +112,8 @@ def manager_loop(self): while self.running: sleep(3) - fap_table : FapTable = None # type: ignore - for fap_table in self.fap_tables.getCopyOfList() : + fap_table: FapTable = None # type: ignore + for fap_table in self.fap_tables.getCopyOfList(): # TODO in view??? displayed? @@ -123,7 +124,9 @@ def manager_loop(self): elif fap_table.status_sync == FapTable.STATUS_LOAD: cards_json = [] - json_filename = os.path.join(fap_table.table_dir, "ftpositions.json") + json_filename = os.path.join( + fap_table.table_dir, "ftpositions.json" + ) if os.path.isfile(json_filename): print("Loading fapTable json:", json_filename) @@ -131,7 +134,9 @@ def manager_loop(self): cards_json = json.load(f) for cardJson in cards_json: card = FapTableCard() - card.load_from_json_dictionary(cardJson, fap_table.table_dir) + card.load_from_json_dictionary( + cardJson, fap_table.table_dir + ) card.image = self.image_server.create_streamed_image( card.image_path, StreamedImage.QUALITY_ORIGINAL ) @@ -165,7 +170,6 @@ def manager_loop(self): if fap_table.is_displayed: # loadsync - # print("Syncing FT:",fapTable.tableDir) for root, d_names, f_names in os.walk(fap_table.table_dir): f_names = [ @@ -182,7 +186,7 @@ def manager_loop(self): for f_name in f_names: found = False - card : FapTableCard = None # type: ignore + card: FapTableCard = None # type: ignore for card in fap_table.cards: f_path = os.path.join(root, f_name) if card.image_path == f_path: diff --git a/rugivi/fap_table/fap_table_view.py b/rugivi/fap_table/fap_table_view.py index cfb92c7..e06ef22 100755 --- a/rugivi/fap_table/fap_table_view.py +++ b/rugivi/fap_table/fap_table_view.py @@ -32,7 +32,7 @@ import pygame from rugivi.fap_table.fap_table import FapTable from rugivi.image_service.streamed_image import StreamedImage -from rugivi.image_service.image_server import * +from rugivi.image_service.abstract_streamed_media import AbstractStreamedMedia class FapTableView: @@ -83,9 +83,6 @@ def is_card_lower_corner(self, card, x, y): image.state == StreamedImage.STATE_READY or image.state == StreamedImage.STATE_READY_AND_RELOADING ): - real_x1 = int((card.x * self.real_w) + self.real_x) - real_y1 = int((card.y * self.real_w) + self.real_y) - real_x2 = int(((card.x + card.width) * self.real_w) + self.real_x) real_y2 = int( ((card.y + (card.width * card.image.aspect_ratio)) * self.real_w) @@ -125,7 +122,7 @@ def draw_fap_table_view(self, display, x, y, w, h): ((card.y + (card.width * image.aspect_ratio)) * w) + y ) - surface : pygame.surface.Surface = image.get_surface() # type: ignore + surface: pygame.surface.Surface = image.get_surface() # type: ignore cat2 = pygame.transform.scale( surface, (real_x2 - real_x1, real_y2 - real_y1) ) diff --git a/rugivi/fap_table/fap_tables.py b/rugivi/fap_table/fap_tables.py index 87b493a..b8028c9 100755 --- a/rugivi/fap_table/fap_tables.py +++ b/rugivi/fap_table/fap_tables.py @@ -32,13 +32,11 @@ import os from rugivi.config_file_handler import ConfigFileHandler -from rugivi.image_service.image_server import * - from rugivi import dir_helper class FapTables: - def __init__(self, configParser : ConfigFileHandler) -> None: + def __init__(self, configParser: ConfigFileHandler) -> None: self.configParser = configParser @@ -79,7 +77,6 @@ def get_all_sub_dirs(self, path): return subdirs def get_current_fap_table_dir(self): - # print("current ft dir"+str(self.fapTables[self.y][self.x])) print("X=", self.x, "Y=", self.y) return self.fap_tables[self.y][self.x] diff --git a/rugivi/image_database_service/image_server_database.py b/rugivi/image_database_service/image_server_database.py index bb3c728..e053082 100755 --- a/rugivi/image_database_service/image_server_database.py +++ b/rugivi/image_database_service/image_server_database.py @@ -30,8 +30,7 @@ # import threading -from threading import Lock -from time import sleep, time +from time import sleep import pygame from sqlitedict import SqliteDict @@ -69,7 +68,7 @@ def to_tuple(self) -> tuple: ) def from_tuple(self, tuple_to_load) -> bool: - """ :return: true, if this ImageProxy could be updated by the tuple_to_load """ + """:return: true, if this ImageProxy could be updated by the tuple_to_load""" self.average_color = tuple_to_load[1] self.aspect_ratio = tuple_to_load[2] self.width = tuple_to_load[3] @@ -82,7 +81,7 @@ def from_tuple(self, tuple_to_load) -> bool: self.image_surface = pygame.image.fromstring( tuple_to_load[0], (self.thumb_width, self.thumb_height), "RGB" ) - except Exception as e: + except Exception: return False return True @@ -121,7 +120,7 @@ def add_image_thumb(self, image: StreamedImage) -> None: self.db[imageProxy.path] = imageProxy.to_tuple() def get_image_proxy_by_path(self, path) -> ImageProxy: - """ :return: ImageProxy on success, None otherwise """ + """:return: ImageProxy on success, None otherwise""" tuple = self.db.get(path) if tuple != None: imageProxy = ImageProxy() @@ -146,7 +145,10 @@ def housekeeping_loop(self): seconds_passed = 0 - while seconds_passed < ImageServerDatabase.DB_COMMIT_EVERY_SECONDS and self.running: + while ( + seconds_passed < ImageServerDatabase.DB_COMMIT_EVERY_SECONDS + and self.running + ): sleep(1) seconds_passed += 1 diff --git a/rugivi/image_service/abstract_cache_filename_generator.py b/rugivi/image_service/abstract_cache_filename_generator.py index 8286bbd..fbd75c1 100644 --- a/rugivi/image_service/abstract_cache_filename_generator.py +++ b/rugivi/image_service/abstract_cache_filename_generator.py @@ -31,12 +31,11 @@ import abc -class AbstractCacheFilenameGenerator: - +class AbstractCacheFilenameGenerator: def __init__(self) -> None: pass @abc.abstractmethod - def generate_new_cache_uuid_and_filename(self) -> (str,str): # type: ignore + def generate_new_cache_uuid_and_filename(self) -> (str, str): # type: ignore pass diff --git a/rugivi/image_service/image_cache.py b/rugivi/image_service/image_cache.py index 1f8a68e..c320918 100755 --- a/rugivi/image_service/image_cache.py +++ b/rugivi/image_service/image_cache.py @@ -33,39 +33,37 @@ import pathlib import uuid -from rugivi.image_service.abstract_cache_filename_generator import AbstractCacheFilenameGenerator - -class ImageCache(AbstractCacheFilenameGenerator): - +from rugivi.image_service.abstract_cache_filename_generator import ( + AbstractCacheFilenameGenerator, +) +class ImageCache(AbstractCacheFilenameGenerator): def __init__(self, cache_base_dir) -> None: self.cache_base_dir = cache_base_dir - def generate_path_for_uuid(self,uuid_name): + def generate_path_for_uuid(self, uuid_name): full_path = self.__get_path_for_uuid(uuid_name) pathlib.Path(full_path).mkdir(parents=True, exist_ok=True) - def generate_new_cache_uuid_and_filename(self) -> (str,str): # type: ignore + def generate_new_cache_uuid_and_filename(self) -> (str, str): # type: ignore uuid_name = uuid.uuid4().hex self.generate_path_for_uuid(uuid_name) full_path = self.__get_path_for_uuid(uuid_name) - return uuid_name, self.__append_filename_to_path(full_path,uuid_name) - + return uuid_name, self.__append_filename_to_path(full_path, uuid_name) + def __get_path_for_uuid(self, uuid_name) -> str: path_level_1 = uuid_name[:2] path_level_2 = uuid_name[2:4] full_path = os.path.join(self.cache_base_dir, path_level_1, path_level_2) - #full_path_and_filename = os.path.abspath(os.path.join(full_path, uuid_name)) return str(full_path) - - def __append_filename_to_path(self, full_path,uuid_name)->str: + def __append_filename_to_path(self, full_path, uuid_name) -> str: return os.path.abspath(os.path.join(full_path, uuid_name + ".jpg")) def get_filename_for_uuid(self, uuid_name) -> str: full_path = self.__get_path_for_uuid(uuid_name) - return self.__append_filename_to_path(full_path,uuid_name) + return self.__append_filename_to_path(full_path, uuid_name) def file_exists(self, uuid_name): - return os.path.isfile(self.get_filename_for_uuid(uuid_name)) \ No newline at end of file + return os.path.isfile(self.get_filename_for_uuid(uuid_name)) diff --git a/rugivi/image_service/image_server.py b/rugivi/image_service/image_server.py index ec4c689..078ac40 100755 --- a/rugivi/image_service/image_server.py +++ b/rugivi/image_service/image_server.py @@ -31,7 +31,6 @@ import hashlib import math -import os from typing import NoReturn import pygame @@ -48,10 +47,10 @@ from .streamed_image import StreamedImage from ..world_things.chunk import Chunk -from ..thread_safe_list import * +from ..thread_safe_list import ThreadSafeList from ..view import View -from ..world_things.world import * -from ..image_database_service.image_server_database import * +from ..world_things.world import World +from ..image_database_service.image_server_database import ImageServerDatabase class ImageServerConduit: @@ -124,7 +123,6 @@ def conduit_loader_loop(self) -> NoReturn: # image is a surrogate for a video file if self.media.get_extended_attribute("video_still_uuid") != None: - # print("Creating cached file for",self.media.get_extended_attribute("video_file")) uuid_name = self.media.get_extended_attribute( "video_still_uuid" ) @@ -171,14 +169,14 @@ def conduit_loader_loop(self) -> NoReturn: self.media.state = StreamedImage.STATE_LOADED self.image_server._total_disk_loaded += 1 - except pygame.error as message: + except pygame.error: print( "Conduit " + str(self.name) + " cannot load ", self.media.original_file_path, ) self.media.state = StreamedImage.STATE_ERROR_ON_LOAD self.waiting = True - except FileNotFoundError as e: + except FileNotFoundError: print( "Conduit " + str(self.name) + " cannot load ", self.media.original_file_path, @@ -195,7 +193,8 @@ def conduit_loader_loop(self) -> NoReturn: # QUALITY_COLOR self.media.average_color = pygame.transform.average_color(original_surface) # type: ignore - # TODO change load + available quality based on size of image (e.g. image smaller than ordered "screen" size => keep original + # TODO change load + available quality based on size of image + # (e.g. image smaller than ordered "screen" size => keep original # QUALITY_THUMB ... QUALITY_SCREEN if self.media._load_quality >= StreamedImage.QUALITY_THUMB: @@ -319,9 +318,7 @@ def __init__( self.image_cache = ImageCache(cache_base_dir) - # self.video_still_generator = VideoStillGenerator(jpg_quality=65, max_dimension=(800,800),remove_letterbox=True) self.video_still_generator = VideoStillGenerator() - # self.video_still_generator = VideoStillGenerator(jpg_quality=95,remove_letterbox=True) self.image_server_database = ImageServerDatabase(thumbDbFile) for conduit in self.conduits: @@ -388,7 +385,6 @@ def server_loader_loop(self) -> None: > ImageServer.HOUSEKEEPING_MAX_MEM_MB_THRESHOLD * 1024 * 1024 ): pass - # housekeepingNow = True # TODO when more than MB RAM => panic if it will not go down after gc => then housekeeping IS ALWAYS RUNNING self.print_statistic() @@ -430,7 +426,6 @@ def server_view_fetcher_loop(self) -> None: self.fetcher_loop_running = True while self.running: sleep(0.2) - # print("server view fetcher loop") for view in self.views: sleep(1) @@ -449,12 +444,9 @@ def server_view_fetcher_loop(self) -> None: view_chunk_y2_C = view.world.convert_S_to_C(view.world_y2_S) # based on height, fetch surrounding chunks also - # print(view_chunk_x1_C,view_chunk_y1_C,"-",view_chunk_x2_C,view_chunk_y2_C) for x_C in range(view_chunk_x1_C, view_chunk_x2_C + 1): for y_C in range(view_chunk_y1_C, view_chunk_y2_C + 1): chunk: Chunk = view.world.get_chunk_at_C(x_C, y_C) - # print("view fetcher: chunk",chunk.x_C,chunk.y_C) - # sleep(0.02) for x_SL in range(0, World.CHUNK_SIZE): sleep(0.00002) for y_SL in range(0, World.CHUNK_SIZE): @@ -467,17 +459,8 @@ def server_view_fetcher_loop(self) -> None: if isinstance(image, StreamedMockup): continue - # needed_quality = StreamedImage.QUALITY_THUMB needed_quality = StreamedImage.QUALITY_GRID - # if ( - # view.height / 4 - # < World.SPOT_SIZE - # / ImageServer.QUALITY_PIXEL_SIZE[ - # StreamedImage.QUALITY_THUMB - # ] - # ): - # needed_quality = StreamedImage.QUALITY_GRID x_S = x_SL + chunk.top_spot_x_S y_S = y_SL + chunk.top_spot_y_S is_on_screen = ( @@ -616,7 +599,6 @@ def create_streamed_image( def create_streamed_mockup(self, path, color_string="", color=None): image = StreamedMockup() image.set_original_file_path(path) - # self.media_queue.put(image) if color != None: image.average_color = color else: diff --git a/rugivi/image_service/video_still_generator.py b/rugivi/image_service/video_still_generator.py index d8a3d5b..8c524e3 100755 --- a/rugivi/image_service/video_still_generator.py +++ b/rugivi/image_service/video_still_generator.py @@ -29,65 +29,16 @@ ############################################################################################## # -import gc import math import os -import time import cv2 -from threading import Lock -class VideoStillGenerator: +class VideoStillGenerator: def __init__(self, jpg_quality=75, max_dimension=(None, None), remove_letterbox=False) -> None: # type: ignore self.jpg_quality = jpg_quality # 0-100, default of cv2 = 90 self.max_dimension = max_dimension self.remove_letterbox = remove_letterbox - #self._lock = Lock() - #self._lock2 = Lock() - - #self.vcs = {} - #self.vcs_time = {} - #self.vcs_queue = [] - - ''' - def get_vc_for_videofile(self, videofile): - with self._lock2: - if videofile in self.vcs: - self.vcs_queue.remove(videofile) - self.vcs_queue.insert(0,videofile) - self.vcs_time[videofile] = time.time() - print("VC Cache:",len(self.vcs_queue),videofile) - return self.vcs[videofile] - else: - if len(self.vcs_queue) >= 40: - videofile_to_remove = self.vcs_queue.pop() - the_vcs = self.vcs.pop(videofile_to_remove, None) - the_vcs.release() - videofile_to_remove.release() - print("VC destr:",len(self.vcs_queue),videofile_to_remove) - - - #to_be_removed = [] - #for vf, last_access in self.vcs_time: - # if last_access + 60*5 < time.time(): - # to_be_removed.append(vf) - #for vf in to_be_removed: - # the_vcs = self.vcs.pop(vf, None) - # the_vcs.release() - # self.vcs_time.pop(vf,None) - # self.vcs_queue.remove(vf) - # vf.release() - # print("VC destr:",len(self.vcs_queue),vf) - - - new_vcs = cv2.VideoCapture(videofile) - self.vcs_queue.insert(0,videofile) - self.vcs[videofile] = new_vcs - self.vcs_time[videofile] = time.time() - print("VC New :",len(self.vcs_queue),videofile) - return new_vcs - ''' - def __image_resize( self, image, width=None, height=None, inter=cv2.INTER_AREA, upscale=True @@ -99,12 +50,12 @@ def __image_resize( elif width is None: if h <= height: return image - r = height / float(h) # type: ignore + r = height / float(h) # type: ignore dim = (int(w * r), height) elif height is None: if w <= width: return image - r = width / float(w) # type: ignore + r = width / float(w) # type: ignore dim = (width, int(h * r)) else: if w <= width and h <= height: @@ -118,33 +69,22 @@ def __image_resize( r = width / float(w) dim = (width, int(h * r)) - resized = cv2.resize(image, dim, interpolation=inter) # type: ignore + resized = cv2.resize(image, dim, interpolation=inter) # type: ignore return resized - def analyze_and_get_positions(self, videofile): - #print("Video:" + videofile) - #with self._lock: try: - #video = self.get_vc_for_videofile(videofile) video = cv2.VideoCapture(videofile) if not video.isOpened(): - #video.release # save some memory - #del video - #gc.collect() - return [] fps = video.get(cv2.CAP_PROP_FPS) frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) duration = frame_count / fps - #print("Duration:" + str(duration)) - positions = [] - FINAL_DISTANCE = 15 # seconds REACH_EXPONENT = -0.04 # to lim OVERALL_OFFSET = 0 @@ -160,48 +100,28 @@ def analyze_and_get_positions(self, videofile): current_position += distance position_number += 1 - #video.release - #del video - #gc.collect() return positions - except cv2.error as e: - #print("cv2.error:", e) + except cv2.error: return [] - except Exception as e: - #print("Exception:", e) + except Exception: return [] - - def create_and_write_still_image(self, videofile, position, output_file): - video = None - #buffer = None image = None - #with self._lock: try: - #print(videofile,"1",position,time.time()) - #video = self.get_vc_for_videofile(videofile) - video = cv2.VideoCapture(videofile, apiPreference=cv2.CAP_FFMPEG) # TODO use ffmpeg faster? how to install? + video = cv2.VideoCapture( + videofile, apiPreference=cv2.CAP_FFMPEG + ) # TODO use ffmpeg faster? how to install? if not video.isOpened(): - video.release # save some memory - #del video - #gc.collect() - #cv2.destroyAllWindows() + video.release # save some memory return [] - #print(videofile,"2",position,time.time()) - #fps = video.get(cv2.CAP_PROP_FPS) - #print(videofile,"3a",position,time.time()) - #frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - #print(videofile,"3b",position,time.time()) - #duration = frame_count / fps - #print(videofile,"3c",position,time.time()) - video.set(cv2.CAP_PROP_POS_MSEC, position * 1000) # this is slow! (~2-3 sec) - #print(videofile,"3d",position,time.time()) + video.set( + cv2.CAP_PROP_POS_MSEC, position * 1000 + ) # this is slow! (~2-3 sec) hasFrames, image = video.read() - #print(videofile,"4",position,time.time()) if hasFrames: image_resized = self.__image_resize( @@ -210,76 +130,32 @@ def create_and_write_still_image(self, videofile, position, output_file): height=self.max_dimension[1], upscale=False, ) - #del image + # del image image = image_resized if self.remove_letterbox: # remove black borders / letterbox - gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) - _,thresh = cv2.threshold(gray,10,255,cv2.THRESH_BINARY) - x,y,w,h = cv2.boundingRect(thresh) - if w > 10 and h > 10: # prevent cropping of black images - image = image[y:y+h,x:x+w] - - - - # using buffer and imencode to avoid naming the file ".jpg" - #success, buffer = cv2.imencode( - # ".jpg", image, [cv2.IMWRITE_JPEG_QUALITY, self.jpg_quality] - #) + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY) + x, y, w, h = cv2.boundingRect(thresh) + if w > 10 and h > 10: # prevent cropping of black images + image = image[y : y + h, x : x + w] # never overwrite! if not os.path.isfile(output_file): - #buffer.tofile(output_file) # will fail to write when path does not exist! - image_written = cv2.imwrite(output_file, image, [int(cv2.IMWRITE_JPEG_QUALITY), self.jpg_quality]) - #print("wrote",output_file, success,"from","'"+videofile+"'") - #video.release # save some memory - #image.release - #cv2.destroyAllWindows() - #del video - #del image - #del buffer - #gc.collect() - #print(videofile,"5",position,time.time()) + image_written = cv2.imwrite( + output_file, + image, + [int(cv2.IMWRITE_JPEG_QUALITY), self.jpg_quality], + ) return image_written else: print("Cache file exists! Panic!") - - - #video.release # save some memory - #image.release - #del image - #del video - #del buffer - #gc.collect() return False - #video.release return False - except cv2.error as e: - #print("cv2.error:", e) - - #if video != None: - #video.release # save some memory - #if image != None: - # image.release - #del video - #del image - #del buffer - #gc.collect() - - + except cv2.error: return False - except Exception as e: - #print("Exception:", e) - #if video != None: - # video.release # save some memory - #if image != None: - # image.release - #del video - #del image - #del buffer - #gc.collect() + except Exception: return False - diff --git a/rugivi/landmarks/landmark_service.py b/rugivi/landmarks/landmark_service.py new file mode 100644 index 0000000..8e18e2e --- /dev/null +++ b/rugivi/landmarks/landmark_service.py @@ -0,0 +1,51 @@ +#!/usr/bin/python3 +# +############################################################################################## +# +# RuGiVi - Adult Media Landscape Browser +# +# For updates see git-repo at +# https://github.com/pronopython/rugivi +# +############################################################################################## +# +# Copyright (C) PronoPython +# +# Contact me at pronopython@proton.me +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################################## +# + +from rugivi.landmarks.tour import Tour +from rugivi.world_database_service.world_database import WorldDatabase + + +class LandmarkService: + def __init__(self, world_database: WorldDatabase) -> None: + self.world_database = world_database + self.tours = {} + + def load_from_db(self): + pass + + def save_to_db(self): + pass + + def get_tour_by_name(self, tourname) -> Tour: + return None # type: ignore + + def add_tour(self, tour: Tour): + self.tours[tour.name] = tour diff --git a/rugivi/landmarks/point_of_interest.py b/rugivi/landmarks/point_of_interest.py new file mode 100644 index 0000000..e933860 --- /dev/null +++ b/rugivi/landmarks/point_of_interest.py @@ -0,0 +1,57 @@ +#!/usr/bin/python3 +# +############################################################################################## +# +# RuGiVi - Adult Media Landscape Browser +# +# For updates see git-repo at +# https://github.com/pronopython/rugivi +# +############################################################################################## +# +# Copyright (C) PronoPython +# +# Contact me at pronopython@proton.me +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################################## +# + + +class PointOfInterest: + def __init__(self, position=(0, 0), name="", color=(0, 0, 0)) -> None: + self.position = position # in x_S, y_S + self.name = name + self.color = color # in R, G, B + self.tour = None + + def _to_tuple(self): + return (self.position, self.name, self.color) + + def _from_tuple(self, tuple): + self.position = tuple[0] + self.name = tuple[1] + self.color = tuple[2] + + def is_same_spot(self, poi): + if poi is None: + return False + return poi.position == self.position + + def __str__(self) -> str: + return str(self.position) + + def __repr__(self): + return self.__str__() diff --git a/rugivi/landmarks/tour.py b/rugivi/landmarks/tour.py new file mode 100644 index 0000000..cb0ce14 --- /dev/null +++ b/rugivi/landmarks/tour.py @@ -0,0 +1,136 @@ +#!/usr/bin/python3 +# +############################################################################################## +# +# RuGiVi - Adult Media Landscape Browser +# +# For updates see git-repo at +# https://github.com/pronopython/rugivi +# +############################################################################################## +# +# Copyright (C) PronoPython +# +# Contact me at pronopython@proton.me +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################################## +# + +from rugivi.landmarks.point_of_interest import PointOfInterest + + +class Tour: + def __init__( + self, + name="", + insert_deletes_right_side=False, + max_entries=-1, + do_not_add_same_spot=False, + ) -> None: + self.pois: list[PointOfInterest] = [] + self.tour_pois_position = 0 + self.insert_deletes_right_side = insert_deletes_right_side + self.do_not_add_same_spot = do_not_add_same_spot + self.max_entries = max_entries + self.color = (0, 0, 0) + self.name = name + + def __correct_size(self): + while self.max_entries > 0 and len(self.pois) > self.max_entries: + self.pois.pop(0) + self.tour_pois_position -= 1 + + def append(self, poi: PointOfInterest): + if ( + self.do_not_add_same_spot + and ( + (len(self.pois) > 0 and poi.is_same_spot(self.pois[-1])) + or len(self.pois) == 0 + ) + ) or not self.do_not_add_same_spot: + poi.tour = self # type: ignore + self.pois.append(poi) + self.tour_pois_position = len(self.pois) - 1 + self.__correct_size() + + def insert(self, poi: PointOfInterest): + if ( + self.do_not_add_same_spot + and ( + ( + len(self.pois) > 0 + and not poi.is_same_spot(self.pois[self.tour_pois_position]) + ) + or len(self.pois) == 0 + ) + ) or not self.do_not_add_same_spot: + poi.tour = self # type: ignore + if self.insert_deletes_right_side: + if len(self.pois) > 1: + self.pois = self.pois[0 : self.tour_pois_position + 1] + self.pois.append(poi) + self.tour_pois_position = len(self.pois) - 1 + else: + self.pois.insert(self.tour_pois_position, poi) + self.tour_pois_position = len(self.pois) + 1 + self.__correct_size() + + def get_poi(self): + return self.pois[self.tour_pois_position] + + def go_back(self): + if self.tour_pois_position > 0: + self.tour_pois_position -= 1 + + def go_forward(self): + if self.tour_pois_position < len(self.pois) - 1: + self.tour_pois_position += 1 + + def get_current_poi(self) -> PointOfInterest: + if self.has_pois(): + return self.pois[self.tour_pois_position] + else: + return None # type: ignore + + def has_pois(self): + return len(self.pois) > 0 + + def _to_tuple(self): + result = [] + tour_data = ( + self.name, + self.color, + self.max_entries, + self.insert_deletes_right_side, + self.tour_pois_position, + self.do_not_add_same_spot, + ) + result.append(tour_data) + for poi in self.pois: + result.append(poi._to_tuple) + return result + + def _from_tuple(self, tuple): + self.name = tuple[0][0] + self.color = tuple[0][1] + self.max_entries = tuple[0][2] + self.insert_deletes_right_side = tuple[0][3] + self.tour_pois_position = tuple[0][4] + self.do_not_add_same_spot = tuple[0][5] + for poi_tuple in tuple[1:]: + poi = PointOfInterest() + poi._from_tuple(poi_tuple) + self.append(poi) diff --git a/rugivi/print_module_dir.py b/rugivi/print_module_dir.py index 62df973..64f6d8b 100755 --- a/rugivi/print_module_dir.py +++ b/rugivi/print_module_dir.py @@ -31,5 +31,6 @@ import os + def printModuleDir(): print(os.path.dirname(os.path.realpath(__file__))) diff --git a/rugivi/rugivi.py b/rugivi/rugivi.py index 715278b..706d1bc 100755 --- a/rugivi/rugivi.py +++ b/rugivi/rugivi.py @@ -33,9 +33,12 @@ import os import pathlib import sys +from typing import NoReturn +from rugivi.landmarks.tour import Tour from rugivi.rugivi_configurator import ConfigApp +import pkg_resources # Import pygame, hide welcome message because it messes with # status output @@ -54,20 +57,21 @@ import psutil import random -from rugivi.image_service.image_server import * +from rugivi.image_service.image_server import ImageServer from rugivi.image_service.streamed_image import StreamedImage -from rugivi.status import * -from rugivi.world_things.world import * - -from rugivi.world_database_service.world_database import * -from rugivi.dialogs import * -from rugivi.view import * -from rugivi.fap_table.fap_table_manager import * -from rugivi.fap_table.fap_table_view import * -from rugivi.fap_table.fap_table import * -from rugivi.fap_table.fap_tables import * -from rugivi.selection import * +from rugivi.status import Status +from rugivi.world_things.world import World + +from rugivi.dialogs import Dialog_xy +from rugivi.dialogs import Dialog_save_file +from rugivi.dialogs import Dialog_copy_clipboard +from rugivi.view import View +from rugivi.fap_table.fap_table_manager import FapTableManager +from rugivi.fap_table.fap_table_view import FapTableView +from rugivi.fap_table.fap_table import FapTable +from rugivi.fap_table.fap_table import FapTableCard +from rugivi.fap_table.fap_tables import FapTables from rugivi.exports.world_overlook import WorldOverlook import platform @@ -75,12 +79,11 @@ import getopt # commandline arg handler -import tkinter -from tkinter import messagebox - from rugivi import config_file_handler as config_file_handler from rugivi import dir_helper as dir_helper +from time import time + class RugiviMainApp: @@ -110,7 +113,7 @@ def __init__(self) -> None: self.configured = False while not self.configured: - self.configured = dir_helper.is_config_file_present("RuGiVi","rugivi.conf") + self.configured = dir_helper.is_config_file_present("RuGiVi", "rugivi.conf") if self.configured: self.configDir = dir_helper.get_config_dir("RuGiVi") @@ -119,27 +122,20 @@ def __init__(self) -> None: ) try: - self.configured = self.configured and self.configParser.get_boolean("configuration", "configured") + self.configured = self.configured and self.configParser.get_boolean( + "configuration", "configured" + ) except configparser.NoSectionError: self.configured = False if not self.configured and configurator_run: - sys.exit() # Cancel was pressed in configurator + sys.exit() # Cancel was pressed in configurator if not self.configured and not configurator_run: - #root = tkinter.Tk() - #root.withdraw() - #messagebox.showinfo( - # "Not configured", - # "Please configure RuGiVi and apply the settings in the following dialog.", - #) - app = ConfigApp(apply_and_start=True) app.run() - configurator_run = True - self.worldDbFile = self.configParser.get_directory_path( "world", "worldDB", self.worldDbFile ) @@ -176,7 +172,9 @@ def __init__(self) -> None: self.cross_shape_grow = self.configParser.get_boolean("world", "crossshapegrow") self.no_diagonal_grow = self.configParser.get_boolean("world", "nodiagonalgrow") self.organic_grow = self.configParser.get_boolean("world", "organicgrow") - self.reach_out_ant_mode = self.configParser.get_boolean("world", "reachoutantmode") + self.reach_out_ant_mode = self.configParser.get_boolean( + "world", "reachoutantmode" + ) self.vlc_binary = self.configParser.get("videoplayback", "vlcbinary") self.video_crawl_enabled = self.configParser.get_boolean("world", "crawlvideos") @@ -186,22 +184,17 @@ def __init__(self) -> None: self.video_vlc_seek_position = self.configParser.get_boolean( "videoplayback", "vlcseekposition" ) - self.debug_vlc_verbose = self.configParser.get_boolean( - "debug", "vlcverbose" - ) - self.debug_cv2_verbose = self.configParser.get_boolean( - "debug", "cv2verbose" - ) - self.debug_mockupimages = self.configParser.get_boolean( - "debug", "mockupimages" - ) + self.debug_vlc_verbose = self.configParser.get_boolean("debug", "vlcverbose") + self.debug_cv2_verbose = self.configParser.get_boolean("debug", "cv2verbose") + self.debug_mockupimages = self.configParser.get_boolean("debug", "mockupimages") if not self.debug_cv2_verbose: # prevent open cv error messages when video files make problems - os.environ['OPENCV_LOG_LEVEL'] = 'OFF' - os.environ['OPENCV_FFMPEG_LOGLEVEL'] = "-8" + os.environ["OPENCV_LOG_LEVEL"] = "OFF" + os.environ["OPENCV_FFMPEG_LOGLEVEL"] = "-8" - self.video_still_generator_jpg_quality = self.configParser.get_int("videoframe", "jpgquality" + self.video_still_generator_jpg_quality = self.configParser.get_int( + "videoframe", "jpgquality" ) self.video_still_generator_resize_enabled = self.configParser.get_boolean( "videoframe", "maxsizeenabled" @@ -213,7 +206,6 @@ def __init__(self) -> None: "videoframe", "removeletterbox" ) - def parseCommandlineArgs(self) -> None: try: options, args = getopt.getopt( @@ -246,18 +238,24 @@ def open_file(self, path) -> None: def open_video_with_vlc(self, path, position: float = 0.0): if position > 2: position -= 2 - # position_str = "--start-time=" + str(math.floor(position)) position_str = "--start-time=" + str(position) if not self.debug_vlc_verbose: - p = subprocess.Popen([self.vlc_binary, position_str, path],shell=False,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL ) + subprocess.Popen( + [self.vlc_binary, position_str, path], + shell=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) else: - p = subprocess.Popen([self.vlc_binary, position_str, path]) + subprocess.Popen([self.vlc_binary, position_str, path]) def run(self) -> NoReturn: pygame.init() - pygame.display.set_caption("RuGiVi") + self.version = pkg_resources.get_distribution(__package__).version + print("Version:", self.version) + pygame.display.set_caption("RuGiVi" + " " + self.version) icon = pygame.image.load(dir_helper.get_install_dir() + "/icon.png") pygame.display.set_icon(icon) @@ -266,12 +264,21 @@ def run(self) -> NoReturn: self.size, pygame.RESIZABLE | pygame.HWSURFACE | pygame.DOUBLEBUF ) - self.image_server = ImageServer(32, self.thumbDbFile, self.cache_base_dir, number_of_video_conduits = 8) + self.image_server = ImageServer( + 32, self.thumbDbFile, self.cache_base_dir, number_of_video_conduits=8 + ) # reconfigure video still generatorl - self.image_server.video_still_generator.jpg_quality=self.video_still_generator_jpg_quality + self.image_server.video_still_generator.jpg_quality = ( + self.video_still_generator_jpg_quality + ) if self.video_still_generator_resize_enabled: - self.image_server.video_still_generator.max_dimension=(self.video_still_generator_max_dimension,self.video_still_generator_max_dimension) - self.image_server.video_still_generator.remove_letterbox=self.video_still_generator_remove_letterbox + self.image_server.video_still_generator.max_dimension = ( + self.video_still_generator_max_dimension, + self.video_still_generator_max_dimension, + ) + self.image_server.video_still_generator.remove_letterbox = ( + self.video_still_generator_remove_letterbox + ) self.world = World() @@ -283,7 +290,8 @@ def run(self) -> NoReturn: World.SPOT_SIZE / ImageServer.QUALITY_PIXEL_SIZE[StreamedImage.QUALITY_THUMB] ) - view : View = View(self.world, initial_height) + + view: View = View(self.world, initial_height) fap_table_view = FapTableView() @@ -323,10 +331,10 @@ def run(self) -> NoReturn: crawl_videos=self.video_crawl_enabled, excludeDirList=self.excludeDirList, mockup_mode=self.debug_mockupimages, - cross_shape_grow = self.cross_shape_grow, - no_diagonal_grow = self.no_diagonal_grow, - organic_grow = self.organic_grow, - reach_out_ant_mode = self.reach_out_ant_mode, + cross_shape_grow=self.cross_shape_grow, + no_diagonal_grow=self.no_diagonal_grow, + organic_grow=self.organic_grow, + reach_out_ant_mode=self.reach_out_ant_mode, ) self.crawler.run() @@ -346,6 +354,15 @@ def run(self) -> NoReturn: self.world_overlook = None self.save_dialog = None + history_tour = Tour( + "##history_view_1##", + insert_deletes_right_side=True, + max_entries=500, + do_not_add_same_spot=True, + ) + + view.set_history_tour(history_tour) + while self.running: info = pygame.display.Info() view_w_PL, view_h_PL = info.current_w, info.current_h @@ -454,7 +471,7 @@ def run(self) -> NoReturn: clicked_spot_y_S -= 1 view.selection.x_S = clicked_spot_x_S view.selection.y_S = clicked_spot_y_S - view.selection.update_selected_spot() + view.selection.update_selected_spot(update_history=True) redraw = True selection_clock.tick() @@ -500,7 +517,7 @@ def run(self) -> NoReturn: clicked_spot_y_S -= 1 view.selection.x_S = clicked_spot_x_S view.selection.y_S = clicked_spot_y_S - view.selection.update_selected_spot() + view.selection.update_selected_spot(update_history=True) redraw = True selection_clock.tick() @@ -664,29 +681,72 @@ def run(self) -> NoReturn: ) video_position = float(view.selection.image.get_extended_attribute("video_position")) # type: ignore infotext = infotext + "Video file:\n" - infotext = infotext + video_filename + "\n" # type: ignore - infotext = infotext + "Parent directory of video file:\n" - infotext = infotext + str(pathlib.Path(video_filename).parent.resolve()) + "\n" # type: ignore + infotext = infotext + video_filename + "\n" # type: ignore + infotext = ( + infotext + "Parent directory of video file:\n" + ) + infotext = infotext + str(pathlib.Path(video_filename).parent.resolve()) + "\n" # type: ignore infotext = infotext + "Position (sec):\n" infotext = infotext + str(video_position) + "\n" infotext = infotext + "Still image in cache:\n" - infotext = infotext + view.selection.image.original_file_path + "\n" - infotext = infotext + "Parent directory of still image in cache:\n" - infotext = infotext + str(pathlib.Path(view.selection.image.original_file_path).parent.resolve()) + "\n" # type: ignore + infotext = ( + infotext + + view.selection.image.original_file_path + + "\n" + ) + infotext = ( + infotext + + "Parent directory of still image in cache:\n" + ) + infotext = ( + infotext + + str( + pathlib.Path( + view.selection.image.original_file_path + ).parent.resolve() + ) + + "\n" + ) else: infotext = infotext + "Image file:\n" - infotext = infotext + view.selection.get_selected_file() + "\n" - infotext = infotext + "Parent directory of image file:\n" - infotext = infotext + str(pathlib.Path(view.selection.get_selected_file()).parent.resolve()) + "\n" # type: ignore + infotext = ( + infotext + + str(view.selection.get_selected_file()) + + "\n" + ) + infotext = ( + infotext + "Parent directory of image file:\n" + ) + infotext = ( + infotext + + str( + pathlib.Path( + str(view.selection.get_selected_file()) + ).parent.resolve() + ) + + "\n" + ) - infotext = infotext +"Selected Spot:\n" + infotext = infotext + "Selected Spot:\n" infotext = infotext + str(view.selection.x_S) infotext = infotext + "," infotext = infotext + str(view.selection.y_S) + "\n" - infotext = infotext + "Ordered Quality: " + str(view.selection.image.get_ordered_quality()) + ", Available Quality: " + str(view.selection.image.get_available_quality()) + "\n" - infotext = infotext + "State: " + view.selection.image.state + "\n" + infotext = ( + infotext + + "Ordered Quality: " + + str(view.selection.image.get_ordered_quality()) + + ", Available Quality: " + + str(view.selection.image.get_available_quality()) + + "\n" + ) + infotext = ( + infotext + + "State: " + + view.selection.image.state + + "\n" + ) - clipboard_dialog = Dialog_copy_clipboard("Info about selection",infotext) + Dialog_copy_clipboard("Info about selection", infotext) elif event.key == pygame.K_j: last_x_S = math.floor( view.current_center_world_pos_x_P / World.SPOT_SIZE @@ -718,8 +778,46 @@ def run(self) -> NoReturn: view.current_center_world_pos_y_P = ( last_y_S * World.SPOT_SIZE + int(World.SPOT_SIZE / 2) ) + view.selection.x_S = last_x_S + view.selection.y_S = last_y_S + view.selection.update_selected_spot(update_history=True) redraw = True + elif event.key == pygame.K_f: + view.selection.history_tour.go_forward() + if view.selection.history_tour.has_pois(): + ( + x_S, + y_S, + ) = view.selection.history_tour.get_current_poi().position + view.selection.x_S = x_S + view.selection.y_S = y_S + if not view.selection.is_visible(): + view.current_center_world_pos_x_P = ( + x_S * World.SPOT_SIZE + int(World.SPOT_SIZE / 2) + ) + view.current_center_world_pos_y_P = ( + y_S * World.SPOT_SIZE + int(World.SPOT_SIZE / 2) + ) + redraw = True + elif event.key == pygame.K_b: + view.selection.history_tour.go_back() + if view.selection.history_tour.has_pois(): + ( + x_S, + y_S, + ) = view.selection.history_tour.get_current_poi().position + view.selection.x_S = x_S + view.selection.y_S = y_S + if not view.selection.is_visible(): + view.current_center_world_pos_x_P = ( + x_S * World.SPOT_SIZE + int(World.SPOT_SIZE / 2) + ) + view.current_center_world_pos_y_P = ( + y_S * World.SPOT_SIZE + int(World.SPOT_SIZE / 2) + ) + redraw = True + elif event.key == pygame.K_n: if view.selection.get_selected_file() != None: if view.selection.image != None: @@ -816,6 +914,9 @@ def run(self) -> NoReturn: view.current_center_world_pos_y_P = xy_dialog.result[ 1 ] * World.SPOT_SIZE + int(World.SPOT_SIZE / 2) + view.selection.x_S = xy_dialog.result[0] + view.selection.y_S = xy_dialog.result[1] + view.selection.update_selected_spot(update_history=True) xy_dialog = None if self.save_dialog != None and self.save_dialog.result != None: @@ -846,7 +947,7 @@ def run(self) -> NoReturn: view.selection.x_S = clicked_spot_x_S view.selection.y_S = clicked_spot_y_S - view.selection.update_selected_spot() + view.selection.update_selected_spot(update_history=True) # and switch off peek, when selection moves by itself view.selection.peek_enabled = False @@ -954,7 +1055,8 @@ def run(self) -> NoReturn: if ( self.image_server._total_database_loaded + self.image_server._total_disk_loaded - < 10 and not self.debug_mockupimages + < 10 + and not self.debug_mockupimages ): font = pygame.font.SysFont("monospace", 40) @@ -963,7 +1065,6 @@ def run(self) -> NoReturn: mp = int(time() % 4) label2 = font.render((" " * mp) + ".", True, (255, 80, 207)) self.display.blit(label2, (30, 80)) - # pygame.display.flip() if self.running == False: self.display.fill((100, 100, 100, 0)) @@ -986,7 +1087,6 @@ def run(self) -> NoReturn: clock.tick(80) # limits fps font = pygame.font.SysFont("monospace", 50) - # label = font.render(".", 1, (0, 0, 0)) label = font.render(".", True, (0, 0, 0)) self.display.blit(label, (30, 80)) pygame.display.flip() diff --git a/rugivi/rugivi_configurator.py b/rugivi/rugivi_configurator.py index 37528eb..e1ce682 100755 --- a/rugivi/rugivi_configurator.py +++ b/rugivi/rugivi_configurator.py @@ -34,7 +34,6 @@ import pathlib import platform -import sys from tkinter import Checkbutton, IntVar, Tk from tkinter import Label from tkinter import Frame @@ -54,10 +53,8 @@ import tkinter as tk -# import tkinter.ttk as ttk from tkinter import filedialog import os -from typing import NoReturn from rugivi import config_file_handler as config_file_handler from rugivi import dir_helper as dir_helper @@ -102,7 +99,7 @@ def __init__( self.frame = Frame(parent) self.frame.columnconfigure(1, weight=1) - Label(self.frame, text=description,fg=fg).grid(column=0, row=0, sticky=W) + Label(self.frame, text=description, fg=fg).grid(column=0, row=0, sticky=W) self.dirText = StringVar(self.frame, self.initValue) self.dirText.trace_add("write", self.valueChanged) Entry(self.frame, textvariable=self.dirText).grid(column=1, row=0, sticky=W + E) @@ -155,7 +152,13 @@ def loadInitValue(self) -> None: ) def buttonClicked(self) -> None: - filename = filedialog.askopenfilename(filetypes=self.filetypes) + filename = filedialog.asksaveasfilename( + filetypes=self.filetypes, + confirmoverwrite=True, + title=self.description, + initialfile=self.dirText.get(), + initialdir=os.path.dirname(self.dirText.get()), + ) self.dirText.set(filename) self.configParser.change_config()[self.configGroup][self.configItem] = filename @@ -176,7 +179,9 @@ def __init__( self.frame = Frame(parent) self.frame.columnconfigure(1, weight=1) - Label(self.frame, text=description, fg=fg).grid(column=0, row=0, ipadx=5, sticky=W) + Label(self.frame, text=description, fg=fg).grid( + column=0, row=0, ipadx=5, sticky=W + ) self.button = Button( self.frame, text=str(self.initValue), command=self.buttonClicked ) @@ -241,7 +246,6 @@ def __init__( self.listbox = Listbox(listboxframe, height=5) self.listbox.pack(side=LEFT, fill=BOTH, expand=YES) - # self.listbox.grid(column=1,row=0, rowspan=2,sticky=W+E) scrollbar = Scrollbar(listboxframe) scrollbar.pack(side=RIGHT, fill=BOTH) @@ -326,9 +330,13 @@ def __init__(self, apply_and_start=False) -> None: Label(frm, text="Crawler settings").grid(column=0, row=row, sticky=W) row += 1 - # ttk.Button(frm, text="Quit", command=root.destroy).grid(column=1, row=0) itfo = SelectionFolder( - frm, self.configParser, "Images & videos root directory", "world", "crawlerRootDir", fg="blue" + frm, + self.configParser, + "Images & videos root directory", + "world", + "crawlerRootDir", + fg="blue", ) itfo.getFrame().grid(column=0, row=row, ipadx=10, padx=10, sticky=W + E) row += 1 @@ -349,7 +357,7 @@ def __init__(self, apply_and_start=False) -> None: frm, text="You must use a new World DB File or delete the old one when changing root directory", fg="black", - ).grid(column=0, row=row,ipadx=10, padx=10,sticky=W) + ).grid(column=0, row=row, ipadx=10, padx=10, sticky=W) row += 1 Label(frm, text="Thumb Database settings").grid(column=0, row=row, sticky=W) @@ -365,7 +373,12 @@ def __init__(self, apply_and_start=False) -> None: row += 1 itfo = SelectionBoolean( - frm, self.configParser, "Enable video crawling", "world", "crawlvideos", fg="blue" + frm, + self.configParser, + "Enable video crawling", + "world", + "crawlvideos", + fg="blue", ) itfo.getFrame().grid(column=0, row=row, ipadx=10, padx=10, sticky=W + E) row += 1 @@ -437,16 +450,16 @@ def __init__(self, apply_and_start=False) -> None: bframe = Frame(frm) - self.start_menu_entry = IntVar(bframe,value=1) + self.start_menu_entry = IntVar(bframe, value=1) Checkbutton( bframe, text="create start menu entries", variable=self.start_menu_entry ).grid(column=0, row=0, sticky=W) if apply_and_start: - text_apply_and="Apply and start" + text_apply_and = "Apply and start" else: - text_apply_and="Apply and exit" + text_apply_and = "Apply and exit" Button(bframe, text=text_apply_and, command=self.actionSaveAndExit).grid( column=1, row=0, ipadx=5, sticky=E ) @@ -468,7 +481,7 @@ def __init__(self, apply_and_start=False) -> None: def run(self) -> None: self.root.mainloop() - def actionSaveAndExit(self) -> NoReturn: + def actionSaveAndExit(self): self.configParser.change_config()["configuration"]["configured"] = "True" self.configParser.write_changed_config() self.create_directories() @@ -476,12 +489,10 @@ def actionSaveAndExit(self) -> NoReturn: self._create_start_menu_entries() self.root.quit() self.root.destroy() - #exit() - def actionExitWithoutSave(self) -> NoReturn: + def actionExitWithoutSave(self): self.root.quit() self.root.destroy() - #exit() def is_windows(self) -> bool: if platform.system() == "Windows": @@ -678,8 +689,9 @@ def _create_start_menu_entries(self): print("Creating app shortcut...") packagedir = os.path.dirname(os.path.realpath(__file__)) sc_icon = os.path.join(packagedir, "icon.ico") - - # TODO future pyshortcuts versions > 1.9.0 might contain the feature that noexe=True can be used to directly call "rugivi" instead of rugivi.py + + # TODO future pyshortcuts versions > 1.9.0 might contain the feature that noexe=True can be used to + # directly call "rugivi" instead of rugivi.py make_shortcut( script=os.path.join(packagedir, "..\\..\\..\\Scripts\\rugivi.exe"), name="RuGiVi", @@ -691,7 +703,9 @@ def _create_start_menu_entries(self): print("Creating configurator shortcut...") make_shortcut( - script=os.path.join(packagedir, "..\\..\\..\\Scripts\\rugivi_configurator.exe"), + script=os.path.join( + packagedir, "..\\..\\..\\Scripts\\rugivi_configurator.exe" + ), name="RuGiVi Configurator", icon=sc_icon, terminal=False, @@ -704,7 +718,8 @@ def _create_start_menu_entries(self): packagedir = os.path.dirname(os.path.realpath(__file__)) sc_icon = os.path.join(packagedir, "icon.png") - # TODO future pyshortcuts versions > 1.9.0 might contain the feature that noexe=True can be used to directly call "rugivi" instead of rugivi.py + # TODO future pyshortcuts versions > 1.9.0 might contain the feature that noexe=True can be used to + # directly call "rugivi" instead of rugivi.py make_shortcut( script=dir_helper.expand_home_dir("~/.local/bin/rugivi"), name="RuGiVi", diff --git a/rugivi/rugivi_image_cache_maintenance.py b/rugivi/rugivi_image_cache_maintenance.py index 324c33d..17a29d6 100644 --- a/rugivi/rugivi_image_cache_maintenance.py +++ b/rugivi/rugivi_image_cache_maintenance.py @@ -46,12 +46,10 @@ from rugivi import config_file_handler as config_file_handler from rugivi import dir_helper as dir_helper -class ImageCacheMaintenance(): - - +class ImageCacheMaintenance: def __init__(self) -> None: - self.db : SqliteDict= None # type: ignore + self.db: SqliteDict = None # type: ignore self.worldDbFile = "chunks.sqlite" self.cache_base_dir = "./cache" self.cache_files_total_size = 0 @@ -62,9 +60,6 @@ def __init__(self) -> None: self.orphant_files = [] - - - def run(self): self.configDir = dir_helper.get_config_dir("RuGiVi") self.configParser = config_file_handler.ConfigFileHandler( @@ -75,10 +70,11 @@ def run(self): if not self.configured: print("Not configured") - print("Please configure RuGiVi and save the settings with the RuGiVi Configurator before running it") + print( + "Please configure RuGiVi and save the settings with the RuGiVi Configurator before running it" + ) sys.exit(0) - self.worldDbFile = self.configParser.get_directory_path( "world", "worldDB", self.worldDbFile ) @@ -87,108 +83,96 @@ def run(self): "cache", "cacherootdir", self.cache_base_dir ) - - - - - - self.open_world_db(self.worldDbFile) self.generate_world_db_file_list() self.generate_cache_file_list() self.calculate_cache_size() - print("World DB files:",len(self.worldDbFiles)) - print("Cache files :",len(self.cache_files)) - print("Cache size:",int(self.cache_files_total_size/(1024*1024)),"MB") + print("World DB files:", len(self.worldDbFiles)) + print("Cache files :", len(self.cache_files)) + print("Cache size:", int(self.cache_files_total_size / (1024 * 1024)), "MB") self.generate_orphant_file_list() - print("Orphant files :",len(self.orphant_files)) + print("Orphant files :", len(self.orphant_files)) if len(self.orphant_files) > 0: if len(sys.argv) > 1 and sys.argv[1] == "-c": self.delete_orphant_files() else: print("NOTHING WAS CHANGED YET!") - print("RUN THIS TOOL AGAIN with '-c' to move orphant files and clean up the cache!") + print( + "RUN THIS TOOL AGAIN with '-c' to move orphant files and clean up the cache!" + ) else: print("No orphant files. Nothing to clean up. Cache is healthy.") - def open_world_db(self,crawler_db_file): + def open_world_db(self, crawler_db_file): self.db = SqliteDict(crawler_db_file) - - def generate_world_db_file_list(self): - print("Reading Chunks from Database:",end="",flush=True) + print("Reading Chunks from Database:", end="", flush=True) - for item in self.db.items(): # type: ignore + for item in self.db.items(): # type: ignore if str(item[0]).startswith("(") and str(item[0]).endswith(")"): tupel = item[1] if tupel == None: continue - print(".",end="",flush=True) + print(".", end="", flush=True) chunk_save_object = ChunkSaveObject() chunk_save_object.from_tupel(tupel) for filepath in chunk_save_object.spots_filepath_list: if len(filepath) > 0: - #print(filepath) self.worldDbFiles.append(filepath) print("done") - - def generate_cache_file_list(self): - print("Scanning files of cache:",end="",flush=True) + print("Scanning files of cache:", end="", flush=True) every = 5000 for root, d_names, f_names in os.walk(self.cache_base_dir): if root.find("trash_") > -1: print("") - print("Warning: There is already a trash folder present:",root) + print("Warning: There is already a trash folder present:", root) print("") continue for f_name in f_names: if f_name.endswith(".jpg"): file = os.path.join(root, f_name) - #print(file) self.cache_files.append(file) every -= 1 if every <= 0: - print(".",end="",flush=True) + print(".", end="", flush=True) every = 5000 print("done") def calculate_cache_size(self): - print("Gathering information on files in cache:",end="",flush=True) + print("Gathering information on files in cache:", end="", flush=True) every = 5000 self.cache_files_total_size = 0 for file in self.cache_files: self.cache_files_total_size += os.path.getsize(file) every -= 1 if every <= 0: - print(".",end="",flush=True) + print(".", end="", flush=True) every = 5000 print("done") - def generate_orphant_file_list(self): - print("Finding orphant cache files:",end="",flush=True) + print("Finding orphant cache files:", end="", flush=True) every = 5000 for file in self.cache_files: if file not in self.worldDbFiles: self.orphant_files.append(file) every -= 1 if every <= 0: - print(".",end="",flush=True) + print(".", end="", flush=True) every = 5000 print("done") - def delete_orphant_files(self): foldername = "trash_" + time.strftime("%Y%m%d_%H%M%S") @@ -202,26 +186,29 @@ def delete_orphant_files(self): for file in self.orphant_files: # Precaution: Check if file path lies within cache dir if os.path.exists(file) and file.startswith(self.cache_base_dir): - filename = (os.path.basename(file)) - newname = os.path.join(trashbase, filename) + filename = os.path.basename(file) + newname = os.path.join(trashbase, filename) # Precaution: never overwrite if os.path.exists(newname): - print("Panic: Destination file",newname,"already exists! Abort") + print("Panic: Destination file", newname, "already exists! Abort") sys.exit() - os.rename(file,newname) + os.rename(file, newname) moved_files += 1 else: - print("Panic: File",file,"not found or not in cache! Abort") + print("Panic: File", file, "not found or not in cache! Abort") sys.exit() - print(moved_files, "file(s) moved to trash:",trashbase) - print("Check these files if they are ok to be deleted and then delete the 'trash_*' folder manually.") + print(moved_files, "file(s) moved to trash:", trashbase) + print( + "Check these files if they are ok to be deleted and then delete the 'trash_*' folder manually." + ) + if __name__ == "__main__": app = ImageCacheMaintenance() app.run() -def main() -> NoReturn: # type: ignore +def main() -> NoReturn: # type: ignore app = ImageCacheMaintenance() app.run() diff --git a/rugivi/selection.py b/rugivi/selection.py index af42d14..9da9425 100755 --- a/rugivi/selection.py +++ b/rugivi/selection.py @@ -30,12 +30,15 @@ # from typing import Optional + +from rugivi.landmarks.point_of_interest import PointOfInterest +from rugivi.landmarks.tour import Tour from .world_things.world import World from .image_service.abstract_streamed_media import AbstractStreamedMedia class Selection: - def __init__(self, world, view) -> None: + def __init__(self, world, view, history_tour: Tour = None) -> None: # type: ignore self.world: World = world self.view = view @@ -49,20 +52,24 @@ def __init__(self, world, view) -> None: self.frame = None self.image: AbstractStreamedMedia = None # type: ignore + self.history_tour = history_tour + def is_visible(self) -> bool: return ( - self.view.world_x1_S <= self.x_S - and self.view.world_y1_S <= self.y_S - and self.view.world_x2_S >= self.x_S - and self.view.world_y2_S >= self.y_S + self.view.world_x1_S < self.x_S + and self.view.world_y1_S < self.y_S + and self.view.world_x2_S - 1 > self.x_S + and self.view.world_y2_S - 1 > self.y_S ) - def update_selected_spot(self) -> None: + def update_selected_spot(self, update_history=False) -> None: self.frame = self.world.get_frame_at_S(self.x_S, self.y_S) if self.frame != None: self.image = self.frame.image # type: ignore else: - self.image = None # type: ignore + self.image = None # type: ignore + if update_history and self.history_tour is not None: + self.history_tour.insert(PointOfInterest(position=(self.x_S, self.y_S))) def get_selected_file(self) -> Optional[str]: if self.image != None: @@ -72,3 +79,6 @@ def get_selected_file(self) -> Optional[str]: return self.image.original_file_path else: return None + + def set_history_tour(self, history_tour): + self.history_tour = history_tour diff --git a/rugivi/status.py b/rugivi/status.py index 5ab2d0e..b6e0877 100755 --- a/rugivi/status.py +++ b/rugivi/status.py @@ -31,54 +31,29 @@ import pygame -class Status: - +class Status: def __init__(self, fontsize=12): self.fontsize = fontsize self.blackwidth = int(400 * (fontsize / 12)) - self.font = pygame.font.SysFont("monospace",fontsize) + self.font = pygame.font.SysFont("monospace", fontsize) self.lines = [] - - def writeln(self, line): self.lines.append(line) - def draw(self, display): px = 10 py = 10 for line in self.lines: - #text_surf = self.font.render(line, False, (255, 255, 255)).convert() - #text_with_ouline = self.add_outline_to_image(text_surf, 5, (0, 255, 0)) - #display.blit(text_with_ouline, (10, py)) - - - #text = self.font.render(line, False, (0,0,0)).convert() - - #for x in range(px-4, px+4): - # for y in range(py-4,py+4): - # display.blit(text, (x, y)) - - - text = self.font.render(line, True, (0, 0, 0)) - - w = text.get_size()[0] h = text.get_size()[1] if w < self.blackwidth: w = self.blackwidth - temp_surface = pygame.Surface((w,h)) - #temp_surface.fill((26,26,26)) - temp_surface.fill((100,100,100)) + temp_surface = pygame.Surface((w, h)) + temp_surface.fill((100, 100, 100)) temp_surface.blit(text, (0, 0)) - display.blit(temp_surface, (px,py)) - - - #display.blit(text, (px, py)) - + display.blit(temp_surface, (px, py)) py = py + self.fontsize self.lines = [] - diff --git a/rugivi/thread_safe_list.py b/rugivi/thread_safe_list.py index 4bae0e7..8b24b97 100755 --- a/rugivi/thread_safe_list.py +++ b/rugivi/thread_safe_list.py @@ -32,7 +32,7 @@ from threading import Lock -class ThreadSafeList(): +class ThreadSafeList: def __init__(self): self._list = list() self._lock = Lock() diff --git a/rugivi/view.py b/rugivi/view.py index ea7148d..a17bf79 100755 --- a/rugivi/view.py +++ b/rugivi/view.py @@ -33,17 +33,17 @@ from pygame.surface import Surface from .world_things.frame import Frame +from .world_things.world import World -from .world_things.world import * -#from .image_server import * from .image_service.abstract_streamed_media import AbstractStreamedMedia -from .selection import * +from .selection import Selection import random from time import time_ns import math + class View: - def __init__(self, world: World, initial_height : float) -> None: + def __init__(self, world: World, initial_height: float) -> None: self.current_center_world_pos_x_P = 0 self.current_center_world_pos_y_P = 0 self.height = initial_height @@ -127,10 +127,10 @@ def draw_view( spot_width_P = math.ceil(World.SPOT_SIZE / self.height) spot_height_P = math.ceil(World.SPOT_SIZE / self.height) - self.world_x1_S = int(world_x1_P / World.SPOT_SIZE) - 1 - self.world_y1_S = int(world_y1_P / World.SPOT_SIZE) - 1 - self.world_x2_S = int(world_x2_P / World.SPOT_SIZE) + 1 - self.world_y2_S = int(world_y2_P / World.SPOT_SIZE) + 1 + self.world_x1_S = round(world_x1_P / World.SPOT_SIZE) - 1 + self.world_y1_S = round(world_y1_P / World.SPOT_SIZE) - 1 + self.world_x2_S = round(world_x2_P / World.SPOT_SIZE) + 1 + self.world_y2_S = round(world_y2_P / World.SPOT_SIZE) + 1 if ( (self.current_center_world_pos_x_P != self.old_center_world_pos_x_P) @@ -147,11 +147,10 @@ def draw_view( broke_draw_loop = False for drawround in range(0, drawrounds): - if (time_ns() - start_time)/1000000 > 200: # no round longer than 200 ms + if (time_ns() - start_time) / 1000000 > 200: # no round longer than 200 ms broke_draw_loop = True break - self.update_matrix_position += 1 if self.update_matrix_position >= len(self.update_matrix): self.update_matrix_position = 0 @@ -202,7 +201,8 @@ def draw_view( if image == None or ( image.state != AbstractStreamedMedia.STATE_READY - and image.state != AbstractStreamedMedia.STATE_READY_AND_RELOADING + and image.state + != AbstractStreamedMedia.STATE_READY_AND_RELOADING ): pygame.draw.rect( display, @@ -218,7 +218,8 @@ def draw_view( if ( image.state == AbstractStreamedMedia.STATE_READY - or image.state == AbstractStreamedMedia.STATE_READY_AND_RELOADING + or image.state + == AbstractStreamedMedia.STATE_READY_AND_RELOADING ): if ( @@ -279,9 +280,9 @@ def draw_view( ) if spot_width_P < 5: pass - + elif spot_width_P <= 32: - surface :Surface = image.get_surface(AbstractStreamedMedia.QUALITY_THUMB) # type: ignore + surface: Surface = image.get_surface(AbstractStreamedMedia.QUALITY_THUMB) # type: ignore if surface != None: scaled_image_surface = pygame.transform.smoothscale( @@ -299,18 +300,26 @@ def draw_view( self.performance_images_drawn + 1 ) else: - surface :Surface = image.get_surface() # type: ignore + surface: Surface = image.get_surface() # type: ignore if surface != None: # only smoothscale to small destination size, big dest. sizes take too long - if spot_width_P < 256: # TODO hard coded pixels - scaled_image_surface = pygame.transform.smoothscale( - surface, - (image_drawing_width_P, image_drawing_height_P), + if spot_width_P < 256: # TODO hard coded pixels + scaled_image_surface = ( + pygame.transform.smoothscale( + surface, + ( + image_drawing_width_P, + image_drawing_height_P, + ), + ) ) else: scaled_image_surface = pygame.transform.scale( surface, - (image_drawing_width_P, image_drawing_height_P), + ( + image_drawing_width_P, + image_drawing_height_P, + ), ) display.blit( scaled_image_surface, @@ -428,7 +437,8 @@ def draw_view( if image != None: if ( image.state == AbstractStreamedMedia.STATE_READY - or image.state == AbstractStreamedMedia.STATE_READY_AND_RELOADING + or image.state + == AbstractStreamedMedia.STATE_READY_AND_RELOADING ): peek_width_P = peek_size @@ -446,7 +456,7 @@ def draw_view( peek_width_P / image.aspect_ratio ) - surface: pygame.surface.Surface = image.get_surface() # type: ignore + surface: pygame.surface.Surface = image.get_surface() # type: ignore if surface != None: scaled_image_surface = pygame.transform.smoothscale( surface, (image_drawing_width_P, image_drawing_height_P) @@ -454,7 +464,9 @@ def draw_view( display.blit( scaled_image_surface, ( - current_screen_x_PL + spot_width_P + (thickness * 2), + current_screen_x_PL + + spot_width_P + + (thickness * 2), current_screen_y_PL, ), ) @@ -474,3 +486,6 @@ def draw_view( thickness, thickness, ) + + def set_history_tour(self, history_tour): + self.selection.set_history_tour(history_tour) diff --git a/rugivi/world_database_service/abstract_world_database.py b/rugivi/world_database_service/abstract_world_database.py index b7d3ec0..bf893dc 100644 --- a/rugivi/world_database_service/abstract_world_database.py +++ b/rugivi/world_database_service/abstract_world_database.py @@ -33,10 +33,9 @@ from rugivi.world_things.chunk import Chunk - class AbstractWorldDatabase: - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def get_chunk_at_C(self, x_C,y_C) -> Chunk : # type: ignore - pass \ No newline at end of file + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def get_chunk_at_C(self, x_C, y_C) -> Chunk: # type: ignore + pass diff --git a/rugivi/world_database_service/world_database.py b/rugivi/world_database_service/world_database.py index 765b7ea..306fcc2 100755 --- a/rugivi/world_database_service/world_database.py +++ b/rugivi/world_database_service/world_database.py @@ -29,17 +29,15 @@ ############################################################################################## # -from time import time from typing import Any from rugivi.image_service.streamed_image import StreamedImage -from rugivi.image_service.image_server import * +from rugivi.image_service.image_server import ImageServer from rugivi.world_database_service.abstract_world_database import AbstractWorldDatabase from rugivi.world_things.chunk import Chunk from rugivi.world_things.frame import Frame -from rugivi.world_things.world import * - +from rugivi.world_things.world import World from sqlitedict import SqliteDict diff --git a/rugivi/world_things/abstract_world.py b/rugivi/world_things/abstract_world.py index 80e8bf3..2060dce 100644 --- a/rugivi/world_things/abstract_world.py +++ b/rugivi/world_things/abstract_world.py @@ -32,19 +32,20 @@ import abc import math + class AbstractWorld: - __metaclass__ = abc.ABCMeta + __metaclass__ = abc.ABCMeta SPOT_SIZE = 4096 # pixels CHUNK_SIZE = 32 # spots def __init__(self) -> None: pass - def convert_SL_to_S(self,sl,c): - return ( c * AbstractWorld.CHUNK_SIZE ) + sl - + def convert_SL_to_S(self, sl, c): + return (c * AbstractWorld.CHUNK_SIZE) + sl + def convert_S_to_C(self, s: int) -> int: return math.floor(s / AbstractWorld.CHUNK_SIZE) def convert_S_to_SL(self, s: int) -> int: - return s % AbstractWorld.CHUNK_SIZE \ No newline at end of file + return s % AbstractWorld.CHUNK_SIZE diff --git a/rugivi/world_things/chunk.py b/rugivi/world_things/chunk.py index 3b4fbe9..392d25a 100644 --- a/rugivi/world_things/chunk.py +++ b/rugivi/world_things/chunk.py @@ -45,7 +45,9 @@ def __init__(self, world: AbstractWorld, x_C, y_C) -> None: self._spots_matrix = numpy.empty( (AbstractWorld.CHUNK_SIZE, AbstractWorld.CHUNK_SIZE), dtype=object ) - self._number_of_empty_spots = AbstractWorld.CHUNK_SIZE * AbstractWorld.CHUNK_SIZE + self._number_of_empty_spots = ( + AbstractWorld.CHUNK_SIZE * AbstractWorld.CHUNK_SIZE + ) self.world = world self.x_C = x_C self.y_C = y_C @@ -75,7 +77,9 @@ def count_empty_spots(self) -> int: return self._number_of_empty_spots def count_used_spots(self) -> int: - return (AbstractWorld.CHUNK_SIZE * AbstractWorld.CHUNK_SIZE) - self._number_of_empty_spots + return ( + AbstractWorld.CHUNK_SIZE * AbstractWorld.CHUNK_SIZE + ) - self._number_of_empty_spots def is_empty(self) -> bool: - return self.count_used_spots() == 0 \ No newline at end of file + return self.count_used_spots() == 0 diff --git a/rugivi/world_things/frame.py b/rugivi/world_things/frame.py index c18e82c..35b8ada 100644 --- a/rugivi/world_things/frame.py +++ b/rugivi/world_things/frame.py @@ -34,8 +34,8 @@ class Frame: def __init__(self, image=None) -> None: - self.image : AbstractStreamedMedia = image # type: ignore - self.x_S : int = None # type: ignore - self.y_S : int = None # type: ignore - self.x_SL : int = None # type: ignore - self.y_SL : int = None # type: ignore \ No newline at end of file + self.image: AbstractStreamedMedia = image # type: ignore + self.x_S: int = None # type: ignore + self.y_S: int = None # type: ignore + self.x_SL: int = None # type: ignore + self.y_SL: int = None # type: ignore diff --git a/rugivi/world_things/world.py b/rugivi/world_things/world.py index 75b4b25..ddb8dea 100755 --- a/rugivi/world_things/world.py +++ b/rugivi/world_things/world.py @@ -35,7 +35,7 @@ from rugivi.world_things.frame import Frame from threading import Lock from .abstract_world import AbstractWorld -from rugivi.world_things.chunk import Chunk + class World(AbstractWorld): @@ -53,9 +53,7 @@ def __init__(self) -> None: self._lock = Lock() self._chunksInMemory: int = 0 self._numberOfFrames: int = 0 - self.chunkLoader : AbstractWorldDatabase = None # type: ignore - - + self.chunkLoader: AbstractWorldDatabase = None # type: ignore def get_chunk_at_C(self, x_C: int, y_C: int) -> Chunk: with self._lock: @@ -114,5 +112,5 @@ def count_frames(self) -> int: def set_chunk_loader(self, chunkLoader) -> None: self.chunkLoader = chunkLoader - def get_all_chunks_in_memory_as_list(self) -> 'list[Chunk]': + def get_all_chunks_in_memory_as_list(self) -> "list[Chunk]": return list(self._chunks.values()) diff --git a/setup.py b/setup.py index 9339570..50544d9 100755 --- a/setup.py +++ b/setup.py @@ -28,28 +28,44 @@ from setuptools import setup -setup(name='rugivi', - version='0.5.2-alpha', - description='RuGiVi - Adult Media Landscape Browser', - long_description='RuGiVi enables you to fly over your image and video collection and view thousands of images and video frames at once. Zoom in and out from one image to small thumbnails with your mousewheel in seconds. All images are grouped as you have them on your disk and arranged in a huge landscape. RuGiVi can work with hundred thousand of images at once.', - url='https://github.com/pronopython/rugivi', - author='pronopython', - author_email='pronopython@proton.me', - license='GNU GENERAL PUBLIC LICENSE V3', - packages=['rugivi','rugivi.crawlers.first_organic','rugivi.fap_table','rugivi.image_database_service','rugivi.image_service','rugivi.world_database_service','rugivi.world_things','rugivi.exports'], - package_data={'rugivi':['*']}, +setup( + name="rugivi", + version="0.6.0-alpha", + description="RuGiVi - Adult Media Landscape Browser", + long_description="RuGiVi enables you to fly over your image and video collection and view thousands of images and video frames at once. Zoom in and out from one image to small thumbnails with your mousewheel in seconds. All images are grouped as you have them on your disk and arranged in a huge landscape. RuGiVi can work with hundred thousand of images at once.", # noqa E501 + url="https://github.com/pronopython/rugivi", + author="pronopython", + author_email="pronopython@proton.me", + license="GNU GENERAL PUBLIC LICENSE V3", + packages=[ + "rugivi", + "rugivi.crawlers.first_organic", + "rugivi.fap_table", + "rugivi.image_database_service", + "rugivi.image_service", + "rugivi.world_database_service", + "rugivi.world_things", + "rugivi.exports", + "rugivi.landmarks", + ], + package_data={"rugivi": ["*"]}, include_package_data=True, zip_safe=False, - install_requires=['pygame>=2.5.2','psutil>=5.9.8','numpy>=1.26.3','sqlitedict>=2.1.0','pyshortcuts>=1.9.0','Pillow>=9.0.1','opencv-python-headless>=4.9.0.80'], + install_requires=[ + "pygame>=2.5.2", + "psutil>=5.9.8", + "numpy>=1.26.3", + "sqlitedict>=2.1.0", + "pyshortcuts>=1.9.0", + "Pillow>=9.0.1", + "opencv-python-headless>=4.9.0.80", + ], entry_points={ - 'gui_scripts': [ - 'rugivi_configurator=rugivi.rugivi_configurator:main' + "gui_scripts": ["rugivi_configurator=rugivi.rugivi_configurator:main"], + "console_scripts": [ + "rugivi_printModuleDir=rugivi.print_module_dir:printModuleDir", + "rugivi=rugivi.rugivi:main", + "rugivi_image_cache_maintenance=rugivi.rugivi_image_cache_maintenance:main", ], - 'console_scripts': [ - 'rugivi_printModuleDir=rugivi.print_module_dir:printModuleDir', - 'rugivi=rugivi.rugivi:main', - 'rugivi_image_cache_maintenance=rugivi.rugivi_image_cache_maintenance:main' - ] - } - ) - + }, +)