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