diff --git a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/applet.js b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/applet.js index 07b9ec2fced..b7d453e4104 100644 --- a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/applet.js +++ b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/applet.js @@ -1,257 +1,195 @@ const Applet = imports.ui.applet; -const Soup = imports.gi.Soup; -const ByteArray = imports.byteArray; -const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Mainloop = imports.mainloop; const Lang = imports.lang; - -const logging = false; - -let _httpSession; -if (Soup.MAJOR_VERSION == 2) { - _httpSession = new Soup.SessionAsync(); - Soup.Session.prototype.add_feature.call(_httpSession, new Soup.ProxyResolverDefault()); -} else { //version 3 - _httpSession = new Soup.Session(); -} - -const bingHost = 'https://www.bing.com'; -const bingRequestPath = '/HPImageArchive.aspx?format=js&idx=0&n=1&mbl=1'; - -function log(message) { - if (logging) global.log(`[bing-wallpaper@starcross.dev]: ${message}`); -} - +const HttpClient = require("./httpClient").HttpClient; +const FileManager = require("./fileManager").FileManager; +const SettingsManager = require("./settingsManager").SettingsManager; +const Utils = require("./utils").Utils; + +// Time interval for refreshing wallpaper (in seconds) +const REFRESH_INTERVAL = 300; + +/** + * BingWallpaperApplet is a Cinnamon applet that sets the Bing daily wallpaper. + * It handles fetching metadata, downloading images, and updating the desktop background. + */ function BingWallpaperApplet(orientation, panel_height, instance_id) { - this._init(orientation, panel_height, instance_id); + this._init(orientation, panel_height, instance_id); } BingWallpaperApplet.prototype = { - __proto__: Applet.IconApplet.prototype, - - _init: function (orientation, panel_height, instance_id) { - - // Generic Setup - Applet.IconApplet.prototype._init.call(this, orientation, panel_height, instance_id); - this.set_applet_icon_symbolic_name("bing-wallpaper"); - this.set_applet_tooltip('Bing Desktop Wallpaper'); - - // Path to store data in - this.wallpaperDir = `${GLib.get_user_config_dir()}/bingwallpaper`; - let dir = Gio.file_new_for_path(this.wallpaperDir); - if (!dir.query_exists(null)) - dir.make_directory(null); - this.wallpaperPath = `${this.wallpaperDir}/BingWallpaper.jpg`; - this.metaDataPath = `${this.wallpaperDir}/meta.json`; - - // Begin refresh loop - this._refresh(); - }, - - _refresh: function () { - log(`Beginning refresh`); - this._getMetaData(); - this._setTimeout(300); - }, - - _removeTimeout: function () { - if (this._timeout) { - Mainloop.source_remove(this._timeout); - this._timeout = null; - } - }, - - _setTimeout: function (seconds) { - /** Cancel current timeout in event of an error and try again shortly */ - this._removeTimeout(); - log(`Setting timeout (${seconds}s)`); - this._timeout = Mainloop.timeout_add_seconds(seconds, Lang.bind(this, this._refresh)); - }, - - destroy: function () { - this._removeTimeout(); - }, - on_applet_removed_from_panel() { - this._removeTimeout(); - }, - - _getMetaData: function () { - - /** Check for local metadata */ - try { - const jsonString = GLib.file_get_contents(this.metaDataPath)[1]; - const json = JSON.parse(jsonString); - - this.imageData = json.images[0]; - this.set_applet_tooltip(this.imageData.copyright); - log(`Got image url from local file : ${this.imageData.url}`); - - /** See if this data is current */ - const start_date = GLib.DateTime.new( - GLib.TimeZone.new_utc(), - this.imageData.fullstartdate.substring(0,4), - this.imageData.fullstartdate.substring(4,6), - this.imageData.fullstartdate.substring(6,8), - this.imageData.fullstartdate.substring(8,10), - this.imageData.fullstartdate.substring(10,12), - 0 - ); - const end_date = start_date.add_days(1); - const now = GLib.DateTime.new_now_utc(); - - if (now.to_unix() < end_date.to_unix()) { - log('metadata up to date'); - - // Look for image file, check this is up to date - let image_file = Gio.file_new_for_path(this.wallpaperPath); - - if (image_file.query_exists(null)) { - - let image_file_info = image_file.query_info('*', Gio.FileQueryInfoFlags.NONE, null); - let image_file_size = image_file_info.get_size(); - let image_file_mod_secs = image_file_info.get_modification_time().tv_sec; - - if ((image_file_mod_secs > end_date.to_unix()) || !image_file_size) { // Is the image old, or empty? - this._downloadImage(); - - } else { - log("image appears up to date"); - } - - } else { - log("No image file found"); - this._downloadImage(); - } - - } - else { - log('metadata is old, requesting new...'); - this._downloadMetaData(); - } - - - } catch (err) { - log(`Unable to get local metadata ${err}`); - /** File does not exist or there was an error processing it */ - this._downloadMetaData(); - } - }, - - _downloadMetaData: function () { - const process_result = data => { - - // Write to meta data file - let gFile = Gio.file_new_for_path(this.metaDataPath); - let fStream = gFile.replace(null, false, Gio.FileCreateFlags.NONE, null); - let toWrite = data.length; - while (toWrite > 0) - toWrite -= fStream.write(data, null); - fStream.close(null); - - const json = JSON.parse(data); - this.imageData = json.images[0]; - this.set_applet_tooltip(this.imageData.copyright); - log(`Got image url from download: ${this.imageData.url}`); - - this._downloadImage(); - - }; - - // Retrieve json metadata, either from local file or remote - let request = Soup.Message.new('GET', `${bingHost}${bingRequestPath}`); - if (Soup.MAJOR_VERSION === 2) { - _httpSession.queue_message(request, (_httpSession, message) => { - if (message.status_code === 200) { - process_result(message.response_body.data); - } else { - log(`Failed to acquire image metadata (${message.status_code})`); - this._setTimeout(60) // Try again - } - - }); - } else { //version 3 - _httpSession.send_and_read_async(request, Soup.MessagePriority.NORMAL, null, (_httpSession, message) => { - if (request.get_status() === 200) { - const bytes = _httpSession.send_and_read_finish(message); - process_result(ByteArray.toString(bytes.get_data())); - } else { - log(`Failed to acquire image metadata (${request.get_status()})`); - this._setTimeout(60) // Try again - } - }); - } - }, - - _downloadImage: function () { - - log('downloading new image'); - const url = `${bingHost}${this.imageData.url}`; - const regex = /_\d+x\d+./gm; - const urlUHD = url.replace(regex, `_UHD.`); - let gFile = Gio.file_new_for_path(this.wallpaperPath); - - // open the file - let fStream = gFile.replace(null, false, Gio.FileCreateFlags.NONE, null); - - // create a http message - let request = Soup.Message.new('GET', urlUHD); - - // keep track of total bytes written - let bytesTotal = 0; - - if (Soup.MAJOR_VERSION === 2) { - // got_chunk event - request.connect('got_chunk', function (message, chunk) { - if (message.status_code === 200) { // only save the data we want, not content of 301 redirect page - bytesTotal += fStream.write(chunk.get_data(), null); - } - }); - - // queue the http request - _httpSession.queue_message(request, (httpSession, message) => { - // request completed - fStream.close(null); - const contentLength = message.response_headers.get_content_length(); - if (message.status_code === 200 && contentLength === bytesTotal) { - this._setBackground(); - } else { - log("Couldn't fetch image from " + urlUHD); - gFile.delete(null); - this._setTimeout(60) // Try again - } - }); - } else { //version 3 - _httpSession.send_and_read_async(request, Soup.MessagePriority.NORMAL, null, (httpSession, message) => { - if (request.get_status() === 200) { - const bytes = _httpSession.send_and_read_finish(message); - if (bytes && bytes.get_size() > 0) { - fStream.write(bytes.get_data(), null); - } - // request completed - fStream.close(null); - log('Download successful'); - this._setBackground(); - } else { - log("Couldn't fetch image from " + urlUHD); - this._setTimeout(60) // Try again - } - }); - } - }, - - _setBackground: function () { - let gSetting = new Gio.Settings({schema: 'org.cinnamon.desktop.background'}); - const uri = 'file://' + this.wallpaperPath; - gSetting.set_string('picture-uri', uri); - gSetting.set_string('picture-options', 'zoom'); - Gio.Settings.sync(); - gSetting.apply(); + __proto__: Applet.IconApplet.prototype, + + /** + * Initializes the applet, sets up directories, and starts the refresh loop. + * @param {number} orientation - The orientation of the panel. + * @param {number} panel_height - The height of the panel. + * @param {string} instance_id - The instance identifier of the applet. + */ + _init: function (orientation, panel_height, instance_id) { + // Call parent class initialization + Applet.IconApplet.prototype._init.call( + this, + orientation, + panel_height, + instance_id + ); + + // Set applet icon and tooltip + this.set_applet_icon_symbolic_name("bing-wallpaper"); + this.set_applet_tooltip("Bing Desktop Wallpaper"); + + // Initialize file manager and HTTP client + this.fileManager = new FileManager(); + this.httpClient = new HttpClient(); + + this._refresh(); + }, + + /** + * Starts the process to refresh the wallpaper by fetching metadata and setting a timeout. + */ + _refresh: function () { + Utils.log("Beginning refresh"); + this._getMetaData(); + this._setTimeout(REFRESH_INTERVAL); + }, + + /** + * Removes the current timeout to prevent multiple timeouts. + */ + _removeTimeout: function () { + if (this._timeout) { + Mainloop.source_remove(this._timeout); + this._timeout = null; + } + }, + + /** + * Sets a timeout for refreshing the wallpaper. + * @param {number} seconds - Number of seconds to wait before the next refresh. + */ + _setTimeout: function (seconds) { + this._removeTimeout(); + Utils.log(`Setting timeout (${seconds}s)`); + this._timeout = Mainloop.timeout_add_seconds( + seconds, + Lang.bind(this, this._refresh) + ); + }, + + /** + * Cleanup function called when the applet is destroyed or removed. + */ + destroy: function () { + this._removeTimeout(); + }, + + /** + * Called when the applet is removed from the panel to stop refreshing. + */ + on_applet_removed_from_panel() { + this._removeTimeout(); + }, + + /** + * Retrieves metadata from local storage or the Bing server. + */ + _getMetaData: function () { + try { + let metaData = this.fileManager.readMetaData(); + if (this._isMetaDataUpToDate(metaData)) { + Utils.log("Metadata is up-to-date"); + this._checkImage(); + } else { + Utils.log("Metadata is old, requesting new..."); + this._downloadMetaData(); + } + } catch (err) { + Utils.log(`Unable to get local metadata: ${err}`); + this._downloadMetaData(); } + }, + + /** + * Checks if the retrieved metadata is still valid. + * @param {object} metaData - Metadata object containing image details. + * @returns {boolean} - True if metadata is up-to-date, false otherwise. + */ + _isMetaDataUpToDate: function (metaData) { + const { start_date, end_date } = + this.fileManager.parseMetaDataDates(metaData); + const now = GLib.DateTime.new_now_utc(); + return now.to_unix() < end_date.to_unix(); + }, + + /** + * Checks if the image file is up-to-date. If not, it downloads a new image. + */ + _checkImage: function () { + if (this.fileManager.isImageUpToDate()) { + Utils.log("Image appears up-to-date"); + } else { + Utils.log("Image is old or missing"); + this._downloadImage(); + } + }, + + /** + * Downloads metadata from Bing's API and processes it. + */ + _downloadMetaData: function () { + const url = + "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mbl=1"; + this.httpClient.get( + url, + (data) => { + this.fileManager.writeMetaData(data); + this._downloadImage(); + }, + (error) => { + Utils.log(`Failed to acquire image metadata: ${error}`); + this._setTimeout(60); // Retry after 60 seconds + } + ); + }, + + /** + * Downloads the wallpaper image and sets it as the desktop background. + */ + _downloadImage: function () { + Utils.log("Downloading new image"); + const url = this.fileManager.getImageUrl(); + this.httpClient.get( + url, + (data) => { + this.fileManager.writeImage(data); + this._setBackground(); + }, + (error) => { + Utils.log(`Couldn't fetch image from ${url}: ${error}`); + this._setTimeout(60); // Retry after 60 seconds + } + ); + }, + + /** + * Sets the downloaded image as the desktop wallpaper. + */ + _setBackground: function () { + SettingsManager.setWallpaper(this.fileManager.getWallpaperPath()); + }, }; - +/** + * Main function to initialize the BingWallpaperApplet. + * @param {object} metadata - Metadata for the applet. + * @param {number} orientation - The orientation of the panel. + * @param {number} panelHeight - The height of the panel. + * @param {string} instanceId - The instance identifier of the applet. + * @returns {BingWallpaperApplet} - An instance of the BingWallpaperApplet. + */ function main(metadata, orientation, panelHeight, instanceId) { - let bingApplet = new BingWallpaperApplet(orientation, panelHeight, instanceId); - return bingApplet; + return new BingWallpaperApplet(orientation, panelHeight, instanceId); } diff --git a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/fileManager.js b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/fileManager.js new file mode 100644 index 00000000000..43fe93824c6 --- /dev/null +++ b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/fileManager.js @@ -0,0 +1,112 @@ +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; + +/** + * FileManager handles file operations related to storing and retrieving metadata and images. + */ +const FileManager = function () { + this.wallpaperDir = `${GLib.get_user_config_dir()}/bingwallpaper`; + this.wallpaperPath = `${this.wallpaperDir}/BingWallpaper.jpg`; + this.metaDataPath = `${this.wallpaperDir}/meta.json`; + + this._init(); +}; + +FileManager.prototype = { + /** + * Initializes the file manager by creating necessary directories. + */ + _init: function () { + let dir = Gio.file_new_for_path(this.wallpaperDir); + if (!dir.query_exists(null)) dir.make_directory(null); + }, + + /** + * Reads metadata from the local file. + * @returns {object} - Parsed JSON metadata. + */ + readMetaData: function () { + const jsonString = GLib.file_get_contents(this.metaDataPath)[1]; + return JSON.parse(jsonString); + }, + + /** + * Writes metadata to the local file. + * @param {string} data - Metadata in JSON format. + */ + writeMetaData: function (data) { + let gFile = Gio.file_new_for_path(this.metaDataPath); + let fStream = gFile.replace(null, false, Gio.FileCreateFlags.NONE, null); + fStream.write(data, null); + fStream.close(null); + }, + + /** + * Parses metadata dates to GLib.DateTime objects. + * @param {object} metaData - Metadata object containing image details. + * @returns {object} - Object containing start_date and end_date as GLib.DateTime. + */ + parseMetaDataDates: function (metaData) { + const startDateStr = metaData.images[0].fullstartdate; + const start_date = GLib.DateTime.new( + GLib.TimeZone.new_utc(), + startDateStr.substring(0, 4), + startDateStr.substring(4, 6), + startDateStr.substring(6, 8), + startDateStr.substring(8, 10), + startDateStr.substring(10, 12), + 0 + ); + const end_date = start_date.add_days(1); + return { start_date, end_date }; + }, + + /** + * Checks if the image file is up-to-date. + * @returns {boolean} - True if the image is up-to-date, false otherwise. + */ + isImageUpToDate: function () { + let image_file = Gio.file_new_for_path(this.wallpaperPath); + if (image_file.query_exists(null)) { + let image_file_info = image_file.query_info( + "*", + Gio.FileQueryInfoFlags.NONE, + null + ); + let image_file_mod_secs = image_file_info.get_modification_time().tv_sec; + return image_file_mod_secs > GLib.DateTime.new_now_utc().to_unix(); + } + return false; + }, + + /** + * Writes an image to the local file. + * @param {string} data - Image data in binary format. + */ + writeImage: function (data) { + let gFile = Gio.file_new_for_path(this.wallpaperPath); + let fStream = gFile.replace(null, false, Gio.FileCreateFlags.NONE, null); + fStream.write(data, null); + fStream.close(null); + }, + + /** + * Returns the path to the wallpaper file. + * @returns {string} - Path to the wallpaper file. + */ + getWallpaperPath: function () { + return this.wallpaperPath; + }, + + /** + * Returns the image URL for downloading. + * @returns {string} - URL of the image to download. + */ + getImageUrl: function () { + // Implementation depends on the structure of metadata. + // For example: + return this.imageData.url.replace(/_\d+x\d+./gm, `_UHD.`); + }, +}; + +exports.FileManager = FileManager; diff --git a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/httpClient.js b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/httpClient.js new file mode 100644 index 00000000000..7ceb049d31e --- /dev/null +++ b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/httpClient.js @@ -0,0 +1,61 @@ +const Soup = imports.gi.Soup; +const ByteArray = imports.byteArray; +const Lang = imports.lang; + +/** + * HttpClient handles HTTP requests to fetch data from the web. + */ +const HttpClient = function () { + this._init(); +}; + +HttpClient.prototype = { + /** + * Initializes the HTTP client based on the version of Soup. + */ + _init: function () { + this.session = + Soup.MAJOR_VERSION === 2 ? new Soup.SessionAsync() : new Soup.Session(); + if (Soup.MAJOR_VERSION === 2) { + Soup.Session.prototype.add_feature.call( + this.session, + new Soup.ProxyResolverDefault() + ); + } + }, + + /** + * Performs a GET request to the specified URL. + * @param {string} url - The URL to fetch data from. + * @param {function} onSuccess - Callback function to handle successful response. + * @param {function} onError - Callback function to handle errors. + */ + get: function (url, onSuccess, onError) { + let request = Soup.Message.new("GET", url); + if (Soup.MAJOR_VERSION === 2) { + this.session.queue_message(request, (session, message) => { + if (message.status_code === 200) { + onSuccess(message.response_body.data); + } else { + onError(message.status_code); + } + }); + } else { + this.session.send_and_read_async( + request, + Soup.MessagePriority.NORMAL, + null, + (session, message) => { + if (request.get_status() === 200) { + const bytes = this.session.send_and_read_finish(message); + onSuccess(ByteArray.toString(bytes.get_data())); + } else { + onError(request.get_status()); + } + } + ); + } + }, +}; + +exports.HttpClient = HttpClient; diff --git a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/icon.svg b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/icon.svg index eea8edac6c6..7a6cc2296e1 100644 --- a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/icon.svg +++ b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/icon.svg @@ -1,22 +1,61 @@ + + + inkscape:version="0.91 r13725" + sodipodi:docname="bing.svg" + inkscape:export-filename="/home/giulio/Scaricati/bing.svg.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + + + + + id="metadata7"> @@ -27,62 +66,16 @@ - - - - - - - - - - + + + diff --git a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/icons/bing-wallpaper-symbolic.svg b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/icons/bing-wallpaper-symbolic.svg index 8f3ae23b8cd..7a6cc2296e1 100644 --- a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/icons/bing-wallpaper-symbolic.svg +++ b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/icons/bing-wallpaper-symbolic.svg @@ -1,4 +1,6 @@ + + + inkscape:version="0.91 r13725" + sodipodi:docname="bing.svg" + inkscape:export-filename="/home/giulio/Scaricati/bing.svg.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + + + + + id="metadata7"> image/svg+xml - + - - - + + + diff --git a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/settingsManager.js b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/settingsManager.js new file mode 100644 index 00000000000..323a2b35cdf --- /dev/null +++ b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/settingsManager.js @@ -0,0 +1,25 @@ +const Gio = imports.gi.Gio; + +/** + * SettingsManager manages system settings for changing the desktop wallpaper. + */ +const SettingsManager = function () {}; + +SettingsManager.prototype = { + /** + * Sets the desktop wallpaper to the specified file path. + * @param {string} filePath - File path of the wallpaper image. + */ + setWallpaper: function (filePath) { + let gSetting = new Gio.Settings({ + schema: "org.cinnamon.desktop.background", + }); + const uri = "file://" + filePath; + gSetting.set_string("picture-uri", uri); + gSetting.set_string("picture-options", "zoom"); + Gio.Settings.sync(); + gSetting.apply(); + }, +}; + +exports.SettingsManager = SettingsManager; diff --git a/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/utils.js b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/utils.js new file mode 100644 index 00000000000..fa433992a7e --- /dev/null +++ b/bing-wallpaper@starcross.dev/files/bing-wallpaper@starcross.dev/utils.js @@ -0,0 +1,17 @@ +/** + * Utility functions for logging and other common tasks. + */ +const Utils = { + /** + * Logs a message if logging is enabled. + * @param {string} message - The message to log. + */ + log: function (message) { + if (false) { + // Set to true to enable logging + global.log(`[bing-wallpaper@starcross.dev]: ${message}`); + } + }, +}; + +exports.Utils = Utils;