开源的 4K 壁纸软件有哪些?

129 天前
 tuoniaoguoce
毕竟不能浪费了这块 4K 的屏幕。
4399 次点击
所在节点    Android
21 条回复
qinghuazs
129 天前
花见?
Magicdove
129 天前
我都到壁纸网站下载到本地看
Frankcox
129 天前
unsplash, wallhaven.cc
julio867
129 天前
以前逛 wallhaven ,现在定期到 unsplash 找自己喜欢的,然后批量下载,自己还专门写了一个浏览器扩展方便获取原图地址,然后迅雷批量下~~😄
xiangshuaaaa
129 天前
@julio867 扩展发布了嘛,白嫖下( bushi )
julio867
129 天前
@xiangshuaaaa 注册开发者比较麻烦,目前仅自己在本地使用~

之前放了一个 wallhaven 的在 github 上:
https://github.com/zoujia/WallhavenAssistant (写的不好,只保证了能用😅)

unsplash 的抽空再把它放到 github 上吧~
since2021
129 天前
偶尔去 reddit 上自己找
mohuani
129 天前
Weyeeep
129 天前
只用过 unsplash
wfhtqp
129 天前
bing wallpaper 每天换新
565656
129 天前
win11 自带了 设置 windows 聚焦每天自动换
timothyye
129 天前
做过一个 bing wallpaper 的开源项目
https://github.com/TimothyYe/bing-wallpaper
fuyun
129 天前
https://www.ifuyun.com/wallpaper 除了壁纸,还有壁纸背后的故事,支持中英文,支持全文搜索。
前台开源( https://github.com/ifuyun/ifuyun.com ),API 闭源。
JensenQian
128 天前
壁纸我用的 steam 上 19 块小红车
不过不认识的别乱下,最近在闹赛博梅毒
senzyo
128 天前
那得推荐一下 https://github.com/rocksdanister/lively ,完全能替代 Wallpaper Engine
nrq
128 天前
楼上的好东西,感谢分享
LitterGopher
128 天前
pexels 和 unsplash 有提供 API, 你自己註冊一個開發者帳號(免費), 然後寫一個腳本自動獲取就可以了.

我自己是使用 shell + crontab 實現定期獲取隨機圖片並設置爲桌面背景.
zhj0326
127 天前
unsplash
1Z3KYa0qBLvei98o
127 天前
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import os from 'os';
import crypto from 'crypto';
import { setWallpaper } from 'wallpaper';
import readline from 'readline';
import { SocksProxyAgent } from 'socks-proxy-agent';

const UNSPLASH_API_URL = 'https://api.unsplash.com/photos/random';
const ACCESS_KEY = 'Z_GQ0Jd3V27mew6lRc1MB-1ojS0e9s9W7B5FPN6XoZc'; // Replace with your Unsplash access key
const INTERVAL = 5 * 60 * 1000; // 5 minutes
const DOWNLOAD_TIMEOUT = 60 * 1000; // 1 minute
const THREAD_COUNT = 4; // Number of threads for parallel download
const RETRY_LIMIT = 3; // Retry limit for failed downloads
const PROGRESS_TIMEOUT = 10 * 1000; // 10 seconds timeout for progress check
const MIN_SPEED = 10; // Minimum speed in KB/s to consider as stalled

// SOCKS5 Proxy setup
const proxyUrl = 'socks5h://127.0.0.1:1080'; // Replace with your SOCKS5 proxy server address and port
const agent = new SocksProxyAgent(proxyUrl);

// Keywords list
const originalKeywords = [
'mountain', 'africa', 'middle east', 'snow', 'travel',
'Caribbean', 'Hubble Space Telescope', 'roman',
'Soviets', 'Aegean Sea',
'Bahrain', 'Eritrea', 'Iran', 'Iraq', 'Israel',
'Jordan', 'Kuwait', 'Lebanon', 'Oman', 'Qatar',
'Saudi Arabia', 'Syria', 'United Arab Emirates', 'Yemen',
// Europe
'Albania', 'Andorra', 'Austria', 'Belarus', 'Belgium',
'Bosnia and Herzegovina', 'Bulgaria', 'Iceland', 'Ireland',
'Italy', 'Kosovo', 'Latvia', 'Lithuania', 'Luxembourg',
'Malta', 'Monaco', 'Moldova', 'Norway', 'Netherlands',
'Portugal', 'Romania', 'Russia', 'San Marino', 'Serbia',
'Cyprus', 'Slovakia', 'Slovenia', 'Spain', 'Switzerland',
'Ukraine', 'United Kingdom', 'Vatican City',
// Asia
'Afghanistan', 'United Arab Emirates', 'Armenia', 'China',
'Georgia', 'India', 'Indonesia', 'Iran', 'Iraq',
'Israel', 'Japan', 'Jordan', 'Kazakhstan', 'South Korea',
'Kuwait', 'Kyrgyzstan', 'Lebanon', 'Maldives', 'Mongolia',
'Myanmar', 'Nepal', 'Macau', 'Malaysia', 'Pakistan',
'Philippines', 'Russia', 'Saudi Arabia', 'Singapore',
'Sri Lanka', 'Syria', 'Tajikistan', 'Thailand', 'Timor-Leste',
'Turkey', 'Turkmenistan', 'Uzbekistan', 'Yemen',
// Caribbean and South America
'Antigua and Barbuda', 'Bahamas', 'Barbados', 'Bolivia',
'Colombia', 'Cuba', 'Dominica', 'Dominican Republic',
'Grenada', 'Guyana', 'Haiti', 'Honduras', 'Jamaica',
'Nicaragua', 'Paraguay', 'Peru', 'Saint Kitts and Nevis',
'Saint Lucia', 'Saint Vincent and the Grenadines', 'Suriname',
'Trinidad and Tobago', 'Venezuela'
];

// Create a copy of the original keywords to keep track of unused ones
let unusedKeywords = [...originalKeywords];

// Function to generate a random hash
function generateHash(length = 8) {
return crypto.randomBytes(length).toString('hex');
}

// Function to get the path for temporary file
function getTempFilePath(partIndex) {
const tempDir = os.tmpdir();
return path.join(tempDir, `wallpaper_part${partIndex}.tmp`);
}

// Function to get a random keyword
function getRandomKeyword() {
if (unusedKeywords.length === 0) {
unusedKeywords = [...originalKeywords]; // Reset the list when all keywords have been used
console.log('All keywords used, resetting keyword list.');
}
const randomIndex = Math.floor(Math.random() * unusedKeywords.length);
const keyword = unusedKeywords[randomIndex];
unusedKeywords.splice(randomIndex, 1); // Remove the keyword from the list
return keyword;
}

async function downloadPart(photoUrl, start, end, partIndex, totalLength, downloadedParts, startTime) {
let retryCount = 0;
let lastProgress = 0;
let progressTimer;
const tempFilePath = getTempFilePath(partIndex);

// Read previous progress
if (fs.existsSync(tempFilePath)) {
downloadedParts[partIndex] = fs.statSync(tempFilePath).size;
}

while (retryCount < RETRY_LIMIT) {
try {
let currentStart = start + downloadedParts[partIndex];

const response = await axios.get(photoUrl, {
headers: {
'Range': `bytes=${currentStart}-${end}`,
},
responseType: 'arraybuffer',
httpAgent: agent, // Apply the SOCKS5 agent
httpsAgent: agent, // Apply the SOCKS5 agent
onDownloadProgress: (progressEvent) => {
const newBytes = progressEvent.loaded - downloadedParts[partIndex];
downloadedParts[partIndex] += newBytes;
updateProgress(downloadedParts, totalLength, startTime);

const elapsedTime = (Date.now() - startTime) / 1000; // in seconds
const speed = (newBytes / 1024 / elapsedTime).toFixed(2); // in KB/s

// Reset progress timer on new data
clearTimeout(progressTimer);
progressTimer = setTimeout(() => {
if (downloadedParts[partIndex] <= lastProgress || speed < MIN_SPEED) {
console.log(`\nPart ${partIndex + 1} is stalled or too slow, retrying...`);
retryCount++;
if (retryCount >= RETRY_LIMIT) {
throw new Error(`Failed to download part ${partIndex + 1} after ${RETRY_LIMIT} attempts.`);
}
lastProgress = downloadedParts[partIndex];
downloadPart(photoUrl, start, end, partIndex, totalLength, downloadedParts, startTime);
}
}, PROGRESS_TIMEOUT);
}
});

// Append data to temp file
fs.writeFileSync(tempFilePath, response.data, { flag: 'a' });
clearTimeout(progressTimer);
break; // Exit the loop if download was successful

} catch (error) {
console.error(`Part ${partIndex + 1} failed, retrying... (${retryCount}/${RETRY_LIMIT})`);
if (retryCount >= RETRY_LIMIT) {
throw new Error(`Failed to download part ${partIndex + 1} after ${RETRY_LIMIT} attempts.`);
}
}
}
}

async function downloadRandomPhoto() {
console.log('Attempting to download a new wallpaper...');

try {
const keyword = getRandomKeyword(); // Select a random keyword
console.log(`Using keyword: ${keyword}`);

const response = await axios.get(UNSPLASH_API_URL, {
headers: { Authorization: `Client-ID ${ACCESS_KEY}` },
params: {
query: keyword,
},
httpAgent: agent, // Apply the SOCKS5 agent
httpsAgent: agent, // Apply the SOCKS5 agent
});

console.log('Received response from Unsplash API.');

const photoUrl = response.data.urls.full;
console.log(`Photo URL: ${photoUrl}`);

const headResponse = await axios.head(photoUrl, { httpAgent: agent, httpsAgent: agent });
const totalLength = parseInt(headResponse.headers['content-length'], 10);
const tempDir = os.tmpdir();
const hash = generateHash();
const fileName = path.join(tempDir, `wallpaper_${hash}.jpg`);
console.log(`Total size: ${totalLength} bytes`);
console.log(`Saving photo to: ${fileName}`);

const partSize = Math.ceil(totalLength / THREAD_COUNT);
const downloadedParts = new Array(THREAD_COUNT).fill(0);
let startTime = Date.now();

const promises = [];

for (let i = 0; i < THREAD_COUNT; i++) {
const start = i * partSize;
const end = Math.min(start + partSize - 1, totalLength - 1);

console.log(`Downloading part ${i + 1}: bytes ${start}-${end}`);

const promise = downloadPart(photoUrl, start, end, i, totalLength, downloadedParts, startTime);
promises.push(promise);
}

await Promise.all(promises);

// Combine all parts into a single file
const writeStream = fs.createWriteStream(fileName);
for (let i = 0; i < THREAD_COUNT; i++) {
const tempFilePath = getTempFilePath(i);
const data = fs.readFileSync(tempFilePath);
writeStream.write(data);
fs.unlinkSync(tempFilePath); // Delete part file after merging
}
writeStream.end();

console.log('\nPhoto download complete. Setting wallpaper...');

setWallpaper(fileName).then(() => {
console.log('Wallpaper updated successfully!');
}).catch(err => {
console.error('Failed to set wallpaper:', err);
});

} catch (error) {
console.error('Error in downloadRandomPhoto function:', error);
}
}

function updateProgress(downloadedParts, totalLength, startTime) {
const downloadedLength = downloadedParts.reduce((acc, val) => acc + val, 0);
const percent = ((downloadedLength / totalLength) * 100).toFixed(2);

const elapsedTime = (Date.now() - startTime) / 1000; // in seconds
const speed = (downloadedLength / 1024 / elapsedTime).toFixed(2); // in KB/s

readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(`Download progress: ${percent}% | Speed: ${speed} KB/s`);
}

downloadRandomPhoto();
setInterval(() => {
console.log('Starting new wallpaper update cycle...');
downloadRandomPhoto();
}, INTERVAL);
1Z3KYa0qBLvei98o
127 天前
自己去搞一个 access_key

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://ex.noerr.eu.org/t/1114600

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX