diff --git a/README.md b/README.md index 8a39eb4..0433562 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Wallpaper Changer -![](./logo.png) +Wallpapers from https://wallhaven.cc -Pictures from selected subreddits +> wallpapers are saved to `~/.local/share/wallpapers/` ![](./gui.png) ![](./gui2.png) diff --git a/__init__.py b/__init__.py index 41cfb76..85f1a8a 100644 --- a/__init__.py +++ b/__init__.py @@ -1,209 +1,59 @@ -import random +import os +from typing import Optional +import requests from ovos_bus_client.message import Message -from ovos_utils.parse import match_one -from ovos_workshop.decorators import intent_handler, resting_screen_handler +from ovos_utils.log import LOG +from ovos_utils.xdg_utils import xdg_data_home +from ovos_workshop.decorators import intent_handler from ovos_workshop.intents import IntentBuilder from ovos_workshop.skills import OVOSSkill -from wallpaper_changer import set_wallpaper, get_desktop_environment -from wallpaper_changer.search import latest_reddit, latest_wpcraft, \ - latest_unsplash -class WallpapersSkill(OVOSSkill): - - def initialize(self): - # skill settings defaults - if "auto_detect" not in self.settings: - self.settings["auto_detect"] = True - if "desktop_env" not in self.settings: - self.settings["desktop_env"] = get_desktop_environment() - if "rotate_wallpaper" not in self.settings: - self.settings["rotate_wallpaper"] = True - if "change_mins" not in self.settings: - self.settings["change_mins"] = 30 +def get_wallpapers(query: Optional[str] = None, + cache=True, max_pics: int = 5): + url = "https://wallhaven.cc/api/v1/search" + params = {"sorting": "random", + "categories": "100"} + if query: + params["q"] = query + try: + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + data = response.json()["data"] + except (requests.RequestException, KeyError, ValueError) as e: + LOG.error(f"Error fetching wallpapers: {str(e)}") + return [] + urls = [w["path"] for w in data][:max_pics] + if cache: + paths = [] + # standard path already used by the PHAL plugin + local_wallpaper_storage = os.path.abspath(os.path.join(xdg_data_home(), "wallpapers")) + os.makedirs(local_wallpaper_storage, exist_ok=True) + for u in urls: + LOG.debug(f"Downloading wallpaper: {u}") + pic = requests.get(u).content + p = os.path.join(local_wallpaper_storage, u.split("/")[-1]) + with open(p, "wb") as f: + f.write(pic) + paths.append(p) + return paths + return urls - # imaage sources - if "unsplash" not in self.settings: - self.settings["unsplash"] = False - if "wpcraft" not in self.settings: - self.settings["wpcraft"] = True - subs = ['/r/EarthPorn', '/r/BotanicalPorn', '/r/WaterPorn', - '/r/SeaPorn', - '/r/SkyPorn', '/r/FirePorn', '/r/DesertPorn', - '/r/WinterPorn', - '/r/AutumnPorn', '/r/WeatherPorn', '/r/GeologyPorn', - '/r/SpacePorn', - '/r/BeachPorn', '/r/MushroomPorn', '/r/SpringPorn', - '/r/SummerPorn', - '/r/LavaPorn', '/r/LakePorn', '/r/CityPorn', - '/r/VillagePorn', - '/r/RuralPorn', '/r/ArchitecturePorn', '/r/HousePorn', - '/r/CabinPorn', - '/r/ChurchPorn', '/r/AbandonedPorn', '/r/CemeteryPorn', - '/r/InfrastructurePorn', '/r/MachinePorn', '/r/CarPorn', - '/r/F1Porn', - '/r/MotorcyclePorn', '/r/MilitaryPorn', '/r/GunPorn', - '/r/KnifePorn', - '/r/BoatPorn', '/r/RidesPorn', '/r/DestructionPorn', - '/r/ThingsCutInHalfPorn', '/r/StarshipPorn', - '/r/ToolPorn', - '/r/TechnologyPorn', '/r/BridgePorn', '/r/PolicePorn', - '/r/SteamPorn', - '/r/RetailPorn', '/r/SpaceFlightPorn', '/r/roadporn', - '/r/drydockporn', - '/r/AnimalPorn', '/r/HumanPorn', '/r/EarthlingPorn', - '/r/AdrenalinePorn', '/r/ClimbingPorn', '/r/SportsPorn', - '/r/AgriculturePorn', '/r/TeaPorn', '/r/BonsaiPorn', - '/r/FoodPorn', - '/r/CulinaryPorn', '/r/DessertPorn', '/r/DesignPorn', - '/r/RoomPorn', - '/r/AlbumArtPorn', '/r/MetalPorn', '/r/MoviePosterPorn', - '/r/TelevisionPosterPorn', '/r/ComicBookPorn', - '/r/StreetArtPorn', - '/r/AdPorn', '/r/ArtPorn', '/r/FractalPorn', - '/r/InstrumentPorn', - '/r/ExposurePorn', '/r/MacroPorn', '/r/MicroPorn', - '/r/GeekPorn', - '/r/MTGPorn', '/r/GamerPorn', '/r/PowerWashingPorn', - '/r/AerialPorn', - '/r/OrganizationPorn', '/r/FashionPorn', '/r/AVPorn', - '/r/ApocalypsePorn', '/r/InfraredPorn', '/r/ViewPorn', - '/r/HellscapePorn', '/r/sculptureporn', '/r/HistoryPorn', - '/r/UniformPorn', '/r/BookPorn', '/r/NewsPorn', - '/r/QuotesPorn', - '/r/FuturePorn', '/r/FossilPorn', '/r/MegalithPorn', - '/r/ArtefactPorn', - '/r/AmateurEarthPorn', '/r/AmateurPhotography', - '/r/ArtistOfTheDay', - '/r/BackgroundArt', '/r/Conservation', '/r/EarthPornVids', - '/r/EyeCandy', '/r/FWEPP', '/r/ImaginaryLandscapes', - '/r/ImaginaryWildlands', '/r/IncredibleIndia', - '/r/ITookAPicture', - '/r/JoshuaTree', '/r/NationalGeographic', '/r/Nature', - '/r/NatureGifs', - '/r/NaturePics', '/r/NotSafeForNature', '/r/NZPhotos', - '/r/remoteplaces', '/r/Schweiz', '/r/SpecArt', - '/r/wallpapers', - "/r/InterstellarArt"] - self.subs = [s.split("/")[-1].strip() for s in subs] - self.wpcats = [ - '3d', 'abstract', 'animals', 'anime', "art", "black", "cars", - 'city', - 'dark', 'fantasy', 'flowers', 'food', 'holidays', 'love', - 'macro', - 'minimalism', 'motorcycles', 'music', 'nature', 'other', - 'smilies', - 'space', 'sport', 'hi-tech', 'textures', 'vector', 'words', - '60_favorites' - ] - for c in self.subs: - if c not in self.settings: - self.settings[c] = True - for c in self.wpcats: - if c not in self.settings: - self.settings[c] = True +class WallpapersSkill(OVOSSkill): + def initialize(self): # state trackers self.pic_idx = 0 self.picture_list = [] - - # events - self.log.info("Detected desktop env " + self.settings["desktop_env"]) - - # gui slideshow buttons - self.gui.register_handler(f'wallpaper.next', self.handle_next) - self.gui.register_handler(f'wallpaper.prev', self.handle_prev) - self.register_with_PHAL() - # idle screen - def update_picture(self, query=None): - data = {} - if query is None: - cats = list(self.subs) - random.shuffle(cats) - for c in cats: - if data: - break - idx = self.subs.index(c) - if self.settings[c] and random.choice([True, False]): - wps = latest_reddit(self.subs[idx]) - if wps: - random.shuffle(wps) - self.picture_list = wps - self.pic_idx = 0 - data = wps[0] - data["url"] = "https://www.reddit.com/r/{s}/".format(s=c) - if self.settings["unsplash"] and \ - random.choice([True, False]) and not data: - self.picture_list = latest_unsplash(query, n=3) - data = self.picture_list[0] - data["url"] = "https://source.unsplash.com/1920x1080/?" + query - self.pic_idx = 0 - elif self.settings["wpcraft"] and \ - random.choice([True, False]) and not data: - wps = latest_wpcraft() - random.shuffle(wps) - data = wps[0] - self.picture_list = wps - self.pic_idx = 0 - data["url"] = "https://wallpaperscraft.com" - else: - # fuzzy match voc_files - best_sub = query - best_score = 0 - for s in self.subs: - words = self.voc_list(s, self.lang) - sub, score = match_one(query, words) - if score > best_score: - best_sub = sub - best_score = score - if best_score > 0.85: - query = best_sub - - # select subreddit - if query in self.subs: - wps = latest_reddit(query) - if wps: - random.shuffle(wps) - self.picture_list = wps - self.pic_idx = 0 - data = wps[0] - data["url"] = "https://www.reddit.com/r/{s}/".format(s=query) - elif query in self.wpcats: - wps = latest_wpcraft(query) - random.shuffle(wps) - data = wps[0] - self.picture_list = wps - self.pic_idx = 0 - data["url"] = "https://wallpaperscraft.com/catalog/" + query - else: - # no matching subreddit, search in unsplash - self.picture_list = latest_unsplash(query, n=3) - data = self.picture_list[0] - data["url"] = "https://source.unsplash.com/1920x1080/?" + query - self.pic_idx = 0 - - if not data: - # default source of wallpapers - wps = latest_reddit("wallpapers") - random.shuffle(wps) - data = wps[0] - self.picture_list = wps - self.pic_idx = 0 - data["url"] = "https://www.reddit.com/r/wallpapers/" - - for k in data: - self.gui[k] = data[k] - self.set_context("PhotoUpdated") - return data["imgLink"], data.get("title", "") - - @resting_screen_handler("Wallpapers") - def idle(self, message=None): - image, title = self.update_picture() - self.gui.show_image(image, fill='PreserveAspectFit') + def fetch_wallpapers(self, query=None) -> str: + self.picture_list = get_wallpapers(query) + self.pic_idx = 0 + self.set_context("SlideShow") + return self.picture_list[self.pic_idx] # PHAL wallpaper manager integrations def register_with_PHAL(self): @@ -212,98 +62,58 @@ def register_with_PHAL(self): "provider_display_name": self.name})) self.bus.on(f"{self.skill_id}.get.wallpaper.collection", self.handle_wallpaper_scan) self.bus.on(f"{self.skill_id}.get.new.wallpaper", self.handle_wallpaper_get) - wallpapers = list(self.iter_wallpapers()) + self.fetch_wallpapers() self.bus.emit(Message("ovos.wallpaper.manager.collect.collection.response", {"provider_name": self.skill_id, - "wallpaper_collection": wallpapers})) + "wallpaper_collection": self.picture_list})) def handle_wallpaper_scan(self, message: Message): - wallpapers = list(self.iter_wallpapers()) + self.fetch_wallpapers() self.bus.emit(message.reply("ovos.wallpaper.manager.collect.collection.response", {"provider_name": self.skill_id, - "wallpaper_collection": wallpapers})) + "wallpaper_collection": self.picture_list})) def handle_wallpaper_get(self, message: Message): - url, _ = self.update_picture() + url = self.fetch_wallpapers() self.bus.emit(message.reply("ovos.wallpaper.manager.set.wallpaper", {"provider_name": self.skill_id, "url": url})) # skill internals - def iter_wallpapers(self): - for c in self.subs: - if self.settings[c]: - wps = latest_reddit(c) - for u in wps: - yield u["imgLink"] - def change_wallpaper(self, image): - if self.settings["auto_detect"]: - success = set_wallpaper(image) - else: - # allow user override of wallpaper command - success = set_wallpaper(image, self.settings["desktop_env"]) - if not success: - success = set_wallpaper(image) - # update in homescreen skill / PHAL plugin self.bus.emit(Message("ovos.wallpaper.manager.set.wallpaper", - {"provider_name": self.skill_id, - "url": image})) + {"provider_name": self.skill_id, "url": image})) self.bus.emit(Message("homescreen.wallpaper.set", {"url": image})) - return success - - def display(self): - self.gui.clear() - data = self.picture_list[self.pic_idx] - for k in data: - self.gui[k] = data[k] - title = self.picture_list[self.pic_idx].get("title") - if title: - self.speak(title) - self.gui.show_page("slideshow", override_idle=True) - self.set_context("SlideShow") # intents @intent_handler("wallpaper.random.intent") def handle_random_wallpaper(self, message): - image, title = self.update_picture() - success = self.change_wallpaper(image) - if success: - self.speak_dialog("wallpaper.changed") - else: - self.speak_dialog("wallpaper fail") - self.gui.show_image(image, fill='PreserveAspectFit') + self.speak_dialog("searching_random") + image = self.fetch_wallpapers() + self.change_wallpaper(image) + self.speak_dialog("wallpaper.changed") @intent_handler("picture.random.intent") def handle_random_picture(self, message=None): - self.update_picture() - title = self.picture_list[self.pic_idx].get("title") - if title: - self.speak(title) - self.display() + self.speak_dialog("searching_random") + image = self.fetch_wallpapers() + self.gui.show_image(image) @intent_handler("wallpaper.about.intent") def handle_wallpaper_about(self, message): query = message.data["query"] self.speak_dialog("searching", {"query": query}) - image, title = self.update_picture(query) - success = self.change_wallpaper(image) - if success: - self.speak_dialog("wallpaper.changed") - else: - self.speak_dialog("wallpaper fail") - self.gui.show_image(image, fill='PreserveAspectFit') + image = self.fetch_wallpapers(query) + self.change_wallpaper(image) + self.speak_dialog("wallpaper.changed") @intent_handler("picture.about.intent") def handle_picture_about(self, message=None): query = message.data["query"] self.speak_dialog("searching", {"query": query}) - self.update_picture(query) - title = self.picture_list[self.pic_idx].get("title") - if title: - self.speak(title) - self.display() + image = self.fetch_wallpapers(query) + self.gui.show_image(image) @intent_handler(IntentBuilder("NextPictureIntent") .require("next").optionally("picture") @@ -315,7 +125,9 @@ def handle_next(self, message=None): self.pic_idx = total - 1 self.speak_dialog("no.more.pictures") else: - self.display() + self.acknowledge() + image = self.picture_list[self.pic_idx] + self.gui.show_image(image) @intent_handler(IntentBuilder("PrevPictureIntent") .require("previous").optionally("picture") @@ -326,18 +138,15 @@ def handle_prev(self, message=None): self.pic_idx = 0 self.speak_dialog("no.more.pictures") else: - title = self.picture_list[self.pic_idx].get("title") - if title: - self.speak(title) - self.display() + self.acknowledge() + image = self.picture_list[self.pic_idx] + self.gui.show_image(image) @intent_handler(IntentBuilder("MakeWallpaperIntent") .require("set").require("wallpapers").optionally("picture") .require("SlideShow")) def handle_set_wallpaper(self, message=None): - image = self.picture_list[self.pic_idx]["imgLink"] - success = self.change_wallpaper(image) - if success: - self.speak_dialog("wallpaper.changed") - else: - self.speak_dialog("wallpaper fail") + image = self.picture_list[self.pic_idx] + self.change_wallpaper(image) + self.speak_dialog("wallpaper.changed") + self.gui.release() # let home screen show the wallpaper diff --git a/gui/qt5/slideshow.qml b/gui/qt5/slideshow.qml deleted file mode 100644 index cf16545..0000000 --- a/gui/qt5/slideshow.qml +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2020 by Aditya Mehra - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import QtQuick 2.9 -import QtQuick.Controls 2.3 as Controls -import QtQuick.Layouts 1.4 -import org.kde.kirigami 2.8 as Kirigami -import QtGraphicalEffects 1.0 -import Mycroft 1.0 as Mycroft - -Mycroft.Delegate { - property var imageSource: sessionData.imgLink - property var title: sessionData.title - property var caption: sessionData.caption - - Component.onCompleted: { - comicView.forceActiveFocus() - } - - RowLayout { - anchors.fill: parent - - - Controls.RoundButton { - id: previousButton - Layout.minimumWidth: Kirigami.Units.iconSizes.small - Layout.minimumHeight: width - Layout.fillWidth: true - Layout.fillHeight: true - Layout.maximumWidth: Kirigami.Units.gridUnit * 3 - Layout.maximumHeight: width - Layout.alignment: Qt.AlignVCenter - focus: false - icon.source: "images/leftarrow.svg" - KeyNavigation.right: nextButton - - background: Rectangle { - Kirigami.Theme.colorSet: Kirigami.Theme.Button - radius: width - color: previousButton.activeFocus ? Kirigami.Theme.highlightColor : Qt.rgba(0.2, 0.2, 0.2, 1) - layer.enabled: true - layer.effect: DropShadow { - horizontalOffset: 1 - verticalOffset: 2 - } - } - - onClicked: { - triggerGuiEvent('wallpaper.prev', {}) - } - - Keys.onReturnPressed: { - clicked() - } - } - - - Image { - id: comicView - Layout.fillWidth: true - Layout.fillHeight: true - autoTransform: true - mipmap: true - smooth: true - fillMode: Image.PreserveAspectFit - source: imageSource - focus: true - KeyNavigation.right: nextButton - KeyNavigation.left: previousButton - - } - - Controls.RoundButton { - id: nextButton - Layout.minimumWidth: Kirigami.Units.iconSizes.small - Layout.minimumHeight: width - Layout.fillWidth: true - Layout.fillHeight: true - Layout.maximumWidth: Kirigami.Units.gridUnit * 3 - Layout.maximumHeight: width - Layout.alignment: Qt.AlignVCenter - focus: false - icon.source: "images/rightarrow.svg" - KeyNavigation.left: previousButton - - background: Rectangle { - Kirigami.Theme.colorSet: Kirigami.Theme.Button - radius: width - color: nextButton.activeFocus ? Kirigami.Theme.highlightColor : Qt.rgba(0.2, 0.2, 0.2, 1) - layer.enabled: true - layer.effect: DropShadow { - horizontalOffset: 1 - verticalOffset: 2 - } - } - - onClicked: { - triggerGuiEvent('wallpaper.next', {}) - } - - Keys.onReturnPressed: { - clicked() - } - } - } -} diff --git a/locale/en-us/searching_random.dialog b/locale/en-us/searching_random.dialog new file mode 100644 index 0000000..5e3c231 --- /dev/null +++ b/locale/en-us/searching_random.dialog @@ -0,0 +1 @@ +ok, searching for (pictures|images) \ No newline at end of file diff --git a/logo.png b/logo.png deleted file mode 100644 index 4645228..0000000 Binary files a/logo.png and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 005bc33..288102f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -wallpaper_finder>=0.1.1 ovos-utils>=0.0.38,<1.0.0 ovos-workshop>=0.0.15,<3.0.0 \ No newline at end of file