flickrFollowersChecker

UserScript to check Flickr Followers against list

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name flickrFollowersChecker
// @namespace https://galleryminimal.com/
// @description UserScript to check Flickr Followers against list
// @version 1.02
// @author Marc Barrot.
// @license MIT
// @include http*://*flickr.com/*people/*/contacts/rev/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_log
// @grant GM_registerMenuCommand
// ==/UserScript==

/*

Installation
============
First you need a UserScript manager extension for your browser. The state of the art seems to be Tampermonkey these days.
Works with Firefox and Chrome. Even Edge I'm told. https://www.tampermonkey.net. For Safari, it now looks like the preferred manager
is Stay (extension + app) downloadable from the App Store. https://apps.apple.com/gb/app/stay-web-only-browsing/id1591620171.
This script may one day reside at Greasy Fork: https://greasyfork.org/
Click the green Install or Update button at the top left of the page. That's it.
You can now visit or reload one of the Flickr pages the script is modifying: followers.

Release Notes
============
v1.0 check each follower entry against list loaded from galleryminimal.
Adds missing entries to local list.

v1.01 2025-11-14: Added globals display box and user interface.

v1.02 2025-11-15: Added publishing of actual followers list of ids.

*/

// Globals

var glblContactsCount = 0;
var glblFollowersList = undefined;
var glblFollowersListStamp = undefined;
var glblMissingFollowers = [];
var glblActualFollowers = [];
var glblPageNumber = undefined;
var glblLastPage = undefined;
var glblLastCheckedVersion = undefined;
var glblVersion = '1.02';
var glblServerVersion = glblVersion;
var glblListUrl = 'https://galleryminimal.com/explore/data/followers-list.json';
var glblScriptVersionUrl = 'https://greasyfork.org/en/scripts/554965-flickrfollowerschecker.json';

const LastPagesXPath = '//span[@class="pages"]/a[last()]';
const CurrentPageXPath = '//span[@class="this-page"]';
const BuddyIconsXPath = '//img[@class="BuddyIconX"]';
const ContactListNamesXPath = '//td[@class="contact-list-name"]/*[1]';
const ContactListFullNamesXPath = '//span[contains(concat(" ", @class, " "), "fullname")]/*[1]';
const ContactListYouThemXPath = '//td[@class="contact-list-youthem"]/*[1]';

// State persistance

function Save() {
    GM_setValue('list', JSON.stringify(glblFollowersList));
    GM_setValue('stamp', glblFollowersListStamp);
}

function Load() {

    var now = new Date(),
		offset = now.getTimezoneOffset(),
		today = new Date(now.getTime() - (offset * 60 * 1000)),
		check = today.toISOString().split('T')[0],
		lastcheckedversion = '',

		onLoadFunction = function(details) {
			glblFollowersList = JSON.parse(details.responseText);
			glblFollowersListStamp = check;
			console.log('2/glblFollowersList: ' + glblFollowersList.length);
			console.log('2/glblFollowersListStamp: ' + glblFollowersListStamp);
			Save();
		};

	glblMissingFollowers = JSON.parse(GM_getValue('missing', '[]'));
	glblActualFollowers = JSON.parse(GM_getValue('actual', '[]'));
	glblFollowersList = JSON.parse(GM_getValue('list', '[]'));
	glblFollowersListStamp = GM_getValue('stamp', '');
	console.log('1/glblFollowersList: ' + glblFollowersList.length);
	console.log('1/glblFollowersListStamp: ' + glblFollowersListStamp);

	if (! glblFollowersList || glblFollowersListStamp != check) {
		CallMethod(glblListUrl, [], onLoadFunction);
	}

    glblServerVersion = GM_getValue('serverversion', glblServerVersion);
    lastcheckedversion = GM_getValue('lastcheckedversion');

    if (lastcheckedversion) {
		glblLastCheckedVersion = new Date();
		glblLastCheckedVersion.setTime(lastcheckedversion);
    }
}

function Clear() {
    var Ok = confirm('Do you really want to delete followers data ?');

    if (!Ok) {
    	return;
    }

    GM_setValue('list', '[]');
    GM_setValue('stamp', '');
    GM_setValue('lastcheckedversion', '');
    GM_setValue('serverversion', '');
	GM_setValue('missing', '[]');
	GM_setValue('actual', '[]');
    GM_setValue('running', '');
    GM_setValue('page', '');
}

function Show() {

    var newDiv = document.createElement('div'),

    	htmlTemplate = `
<div id="flickrfollowersconfig-main">
	<h3>Flickr Follower Checker</h3>**UPDATE**
	<p>
		<small>Version **VERSION** &copy; copyright 2025 Marc Barrot. Released under <a href="https://opensource.org/licenses/MIT" target="_blank">MIT License</a>.</small>
	</p>
	<table id="FollowerCheckerVars">
		<tr>
			<td class="col1">
				<a href="https://greasyfork.org/en/scripts/554965-flickrfollowerschecker" target="_blank">ServerVersion</a>:
			</td>
			<td class="col2">
				**SERVERVERSION**
			</td>
		</tr>
		<tr>
			<td class="col1">
				LastCheckedVersion:
			</td>
			<td class="col2">
				**LASTCHECKEDVERSION**
			</td>
		</tr>
		<tr>
			<td class="col1">
				ConfigList:
			</td>
			<td class="col2">
				**CONFIGLIST** followers
			</td>
		</tr>
		<tr>
			<td class="col1">
				ConfigListStamp:
			</td>
			<td class="col2">
				**CONFIGLISTSTAMP**
			</td>
		</tr>
		<tr>
			<td class="col1">
				PageNumber:
			</td>
			<td class="col2">
				**PAGENUMBER**
			</td>
		</tr>
		<tr>
			<td class="col1">
				Missing Count:
			</td>
			<td class="col2">
				**MISSING**
			</td>
		</tr>
		<tr>
			<td class="col1">
				Actual Count:
			</td>
			<td class="col2">
				**ACTUAL**
			</td>
		</tr>
	</table>
	<table id="FollowerCheckerData">
		<tr>
			<td class="col1">
				Missing Data:
			</td>
			<td class="col2">
				<textarea id="FollowerData" name="FollowerData">**MISSINGDATA**</textarea>
			</td>
		</tr>
	</table>
	<table id="FollowerCheckerActuals">
		<tr>
			<td class="col1">
				Actuals Data:
			</td>
			<td class="col2">
				<textarea id="FollowerActuals" name="FollowerActuals">**ACTUALSDATA**</textarea>
			</td>
		</tr>
	</table>
	<table id="FollowerCheckerActions">
		<tr>
			<td>
				<a href="#flickrfollowersconfig-clear" class="FollowerCheckerButton">Clear</a>
			</td>
			<td>
				<a href="#flickrfollowersconfig-save" class="FollowerCheckerButton">Save</a>
			</td>
			<td width="100%" align="right">
				<a href="#flickrfollowersconfig-cancel" class="FollowerCheckerButton">Cancel</a>
				<a href="#flickrfollowersconfig-run" class="FollowerCheckerButton">Run</a>
			</td>
		</tr>
	</table>
</div>
		`,

    	cssTemplate = `
#flickrfollowersconfig-main {
	position: fixed;
	top: **TOP**px;
	left: **LEFT**px;
	border: thin solid;
	width: 600px;
	padding: 20px;
	font-family: Arial;
	text-align: left;
	background-color: #eee;
	color: black;
	z-index: 10000;
}

#FollowerCheckerVars,
#FollowerCheckerData,
#FollowerCheckerActuals,
#FollowerCheckerActions {
	width: 580px;
	margin-top: 10px;
	border-top: 1px solid #ccc; ! important;
	padding-top: 10px;
}

#FollowerCheckerVars tr::before,
#FollowerCheckerData tr::before,
#FollowerCheckerActuals,
#FollowerCheckerActions tr::before {
	content: none; ! important;
}

#FollowerCheckerVars tr::after,
#FollowerCheckerData tr::after,
#FollowerCheckerActuals,
#FollowerCheckerActions tr::after {
	content: none; ! important;
}

#FollowerCheckerActions {
	border-top: 1px solid #ccc; ! important;
}

td.col1 {
	width: 120px;
}

#FollowerCheckerData td {
	vertical-align: top;
}

#FollowerData {
	width: 100%;
	height: 6em;
	border: none; ! important;
}

#FollowerActuals {
	width: 100%;
	height: 1.2em;
	border: none; ! important;
}
		`;

	cssTemplate = cssTemplate.replace('**TOP**', document.body.scrollTop + 60);
	cssTemplate = cssTemplate.replace('**LEFT**', document.body.scrollLeft + document.body.clientWidth - 650);
    AddGlobalStyle(cssTemplate);

	htmlTemplate = htmlTemplate.replace('**VERSION**', glblVersion);
	htmlTemplate = htmlTemplate.replace('**UPDATE**', (NewerVersion(glblServerVersion, glblVersion)) ? '<p><b>Please update to <a href="https://greasyfork.org/en/scripts/441654-flickrFollowerChecker">version ' + glblServerVersion + '</a></b>.</p>' : '');
	htmlTemplate = htmlTemplate.replace('**SERVERVERSION**', glblServerVersion);
	htmlTemplate = htmlTemplate.replace('**LASTCHECKEDVERSION**', glblLastCheckedVersion);
	htmlTemplate = htmlTemplate.replace('**CONFIGLIST**', glblFollowersList.length);
	htmlTemplate = htmlTemplate.replace('**CONFIGLISTSTAMP**', glblFollowersListStamp);
	htmlTemplate = htmlTemplate.replace('**PAGENUMBER**', glblPageNumber + ' / ' + glblLastPage + ' (' + GM_getValue('page', '') + ')');
	htmlTemplate = htmlTemplate.replace('**MISSING**', glblMissingFollowers.length);
	htmlTemplate = htmlTemplate.replace('**ACTUAL**', glblActualFollowers.length);
	htmlTemplate = htmlTemplate.replace('**MISSINGDATA**', JSON.stringify(glblMissingFollowers.toReversed(), null, 4));
	htmlTemplate = htmlTemplate.replace('**ACTUALSDATA**', JSON.stringify(glblActualFollowers.toReversed()));
    newDiv.setAttribute('id','flickrfollowersconfig-container');
    newDiv.innerHTML = htmlTemplate;
    document.body.insertBefore(newDiv, document.body.firstChild);
}

// Handle Events

function DeleteElement(elementname) {
    var varElement = document.getElementById(elementname);

    if (varElement) {
        varElement.parentNode.removeChild(varElement);
    }
}

function EventCancel() {
	GM_setValue('running', '');
    DeleteElement("flickrfollowersconfig-container");
}

function EventRun() {

	if (glblPageNumber < glblLastPage) {
		console.log('page: ' + glblPageNumber + ' / ' + glblLastPage);
		GM_setValue('missing', JSON.stringify(glblMissingFollowers));
		GM_setValue('actual', JSON.stringify(glblActualFollowers));
		GM_setValue('running', 'true');
		GM_setValue('page', glblPageNumber);
		window.location = 'https://www.flickr.com/people/marcbarrot/contacts/rev/?page=' + (glblPageNumber + 1);
	}

	else {
		console.log('Last page: ' + glblPageNumber + ' / ' + glblLastPage);
		GM_setValue('running', '');
	}
}

function EventClear() {
	Clear();
    EventCancel();
}

function EventSave() {

    var missing = JSON.parse(document.getElementById('FollowerData').value),
    	actual = JSON.parse(document.getElementById('FollowerActuals').value);

	GM_setValue('missing', JSON.stringify(missing));
	GM_setValue('actual', JSON.stringify(actual));
}

function EventShow() {
    Show();
}

// Configure Event Handler

function ConfigureEvents() {

    document.addEventListener('click', function(event) {
        // event.target is the element that was clicked
        var clickedOn = event.target.toString(),
        	target;

        if (clickedOn.indexOf('#flickrfollowersconfig-') > -1) { // flickrFollowerChecker links

            if (EndsWith(clickedOn, 'clear')) {
                EventClear();
            }

            if (EndsWith(clickedOn, 'save')) {
                EventSave();
            }

            if (EndsWith(clickedOn, 'cancel')) {
                EventCancel();
            }

            if (EndsWith(clickedOn, 'run')) {
                EventRun();
            }

            // we handled the event so stop propagation
            event.stopPropagation();
            event.preventDefault();
        }
    }, true);
}

// API calls

function CallMethod(url, params, onLoadFunction) { // http GET XHR wrapper
	var key;

    for(key in params) {
        url += "&" + key + "=" + params[key];
    }

    GM_xmlhttpRequest({
        method: "GET",
        url: url,

        headers: {
            "User-Agent": "flickrFollowersChecker",
            "Accept": "application/json",
        },

        onload: onLoadFunction
    });
} // http GET XHR wrapper

// Utility functions

function elapsed(d) { //returns number of years/months/weeks/days/hours/min/secs from now

	var now = new Date(),
		from = new Date(parseInt(d, 10) * 1000), // d is the number of seconds since the epoch as a string
		years = now.getFullYear() - from.getFullYear(),
		months = now.getMonth() - from.getMonth(),
		secs = Math.floor((now - from) / 1000),
		mins = Math.floor(secs / 60),
		hours = Math.floor(mins / 60),
		days = Math.floor(hours / 24),
		weeks = Math.floor(days / 7);

	months = (months < 0) ? 12 + months : months;

	if (years && weeks > 52) {
		return years + 'y';
	}

	if (months && days >= 30) {
		return months + 'm';
	}

	if (weeks) {
		return weeks + 'w';
	}

	if (days) {
		return days + 'd';
	}

	if (hours) {
		return hours + 'h';
	}

	if (mins) {
		return mins + 'm';
	}

	if (secs) {
		return secs + 's';
	}

	return 'error';
} //returns number of years/months/weeks/days/hours/min/secs from now

function AddGlobalStyle(css) {
    var head, style;
    head = document.getElementsByTagName('head')[0];
    if (!head) { return; }
    style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = css;
    head.appendChild(style);
}

function DateDiffDays(date1, date2) {
    var one_day = 1000 * 60 * 60 * 24;
    return Math.ceil((date1.getTime() - date2.getTime()) / one_day);
}

function EndsWith(str, substr){
    return (str.lastIndexOf(substr) == str.length - substr.length);
}

// check Greasy Fork for updates

function SaveUpdateInfo() {
    GM_setValue('serverversion', glblServerVersion);

    if (glblLastCheckedVersion) {
    	GM_setValue('lastcheckedversion',glblLastCheckedVersion.getTime().toString());
    }
}

function CheckForUpdates() {

    var onLoadFunction = function(details) {
        var meta = JSON.parse(details.responseText);
        glblServerVersion = meta.version;
        glblLastCheckedVersion = new Date();
        console.log('2/glblServerVersion: ' + glblServerVersion);
        console.log('2/glblLastCheckedVersion: ' + glblLastCheckedVersion);
        SaveUpdateInfo();
    }

    console.log('1/glblServerVersion: ' + glblServerVersion);
    console.log('1/glblLastCheckedVersion: ' + glblLastCheckedVersion);

    var currentDate = new Date();

    if ((glblLastCheckedVersion == undefined) || (DateDiffDays(currentDate, glblLastCheckedVersion) >= 2)) {
        CallMethod(glblScriptVersionUrl, [], onLoadFunction);
    }
}

function NewerVersion(serverVersion, currentVersion) {
    serverVersion = serverVersion.split(".");
    currentVersion = currentVersion.split(".");

    var i;

    for (i = 0; i < serverVersion.length; i++) {
        if (parseInt(serverVersion[i]) > parseInt(currentVersion[i])) return true;
    }

    return false;
}

// DOM access functions

function GetElements(root, xPath) {

    var allElements = document.evaluate(
        xPath,
        root,
        null,
        XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
        null);

    return allElements;
}

function FindFirstElement(root, xPath) {

    var allElements = GetElements(root, xPath),
    	thisElement = null;

    if (allElements.snapshotLength > 0) {
        thisElement = allElements.snapshotItem(0);
    }

    return thisElement;
}

// Contacts page functions

function GetCurrentPageNumber() {

	var pageElement = FindFirstElement(document, CurrentPageXPath),
		pagenb = (pageElement) ? pageElement.innerHTML : '';

	return pagenb;
}

function GetLastPageNumber() {

	var pageElement = FindFirstElement(document, LastPagesXPath),
		last = (pageElement) ? pageElement.innerHTML : '';

	return last;
}

function GetAllContactsFromPage() {

	var iconNodes = GetElements(document, BuddyIconsXPath),
		fullnameNodes = GetElements(document, ContactListFullNamesXPath),
		nameNodes = GetElements(document, ContactListNamesXPath),
		youthemNodes = GetElements(document, ContactListYouThemXPath),
		ids = [],
		id = '',
		names = [],
		name = '',
		fullnames = [],
		fullname = '',
		i = 0;

	if (iconNodes.snapshotLength > 0) {

		for (i = 0; i < iconNodes.snapshotLength; i++) {
			id = iconNodes.snapshotItem(i).getAttribute('src');
			id = id.replace(/.*buddyicons\//, '');
			id = id.replace(/.*#/, '');
			id = id.replace(/\?.*/, '');

			if (! id.includes('@N')) {
				id = iconNodes.snapshotItem(i).parentElement.getAttribute('href');
				id = id.replace(/\/$/, '');
				id = id.replace(/.*\//, '');
			}

			if (! id.includes('@N')) {
				id = youthemNodes.snapshotItem(i).getAttribute('id');
				id = id.replace(/.*status_/, '');
			}

			if (! id.includes('@N')) {
				id = '';
			}

			ids.push(id);
		}

	}

	if (nameNodes.snapshotLength > 0) {

		for (i = 0; i < nameNodes.snapshotLength; i++) {
			name = nameNodes.snapshotItem(i).innerHTML;
			names.push(name);
		}

	}

	if (fullnameNodes.snapshotLength > 0) {

		for (i = 0; i < fullnameNodes.snapshotLength; i++) {
			fullname = fullnameNodes.snapshotItem(i).innerHTML;
			fullname = fullname.replace('No real name given', '');
			fullnames.push(fullname);
		}

	}

	return {'ids': ids, 'names': names, 'fullnames': fullnames };
}

// Processing contacts

function CheckContacts(contacts) {
	var missing = [];

	for (var i = 0; i < contacts.ids.length; i++) {

		if (! glblFollowersList.find((el) => el === contacts.ids[i])) {

			missing.push( {
				'nsid': contacts.ids[i],
				'username': contacts.names[i],
      			'realname': contacts.fullnames[i],
      			'stamp': new Date().getTime()
      		});
		}
	}

	return missing;
}

function ProcessContactsPage() {

	var contacts = GetAllContactsFromPage(),
		missing = CheckContacts(contacts);

	glblPageNumber = parseInt(GetCurrentPageNumber(), 10);
	glblLastPage = parseInt(GetLastPageNumber(), 10);
	glblLastPage = (glblLastPage >= glblPageNumber) ? glblLastPage : glblPageNumber;
	glblMissingFollowers.push(...missing);
	glblActualFollowers.push(...contacts.ids);
	glblContactsCount = contacts.ids.length;
}

// Main script

function Main() {

	var nextPage = function() {
		window.location = 'https://www.flickr.com/people/marcbarrot/contacts/rev/?page=' + (glblPageNumber + 1);
	};

    GM_registerMenuCommand("Show flickrFollowersChecker", EventShow);
    GM_registerMenuCommand("Hide flickrFollowersChecker", EventCancel);
    GM_registerMenuCommand("Clear flickrFollowersChecker", EventClear);
    AddGlobalStyle('.FollowerCheckerButton:link, .FollowerCheckerButton:visited { padding: 2px 4px 2px 4px; margin: 0px; text-decoration: none; text-align: center; border: 1px solid; border-color: #aaa #000 #000 #aaa; background: #BBBBBB !important; }');
    AddGlobalStyle('.FollowerCheckerButtonSmall:link, .FollowerCheckerButtonSmall:visited { padding: 2px 4px 2px 4px; margin: 0px; text-decoration: none; width: 10px; text-align: center; border: 1px solid; border-color: #aaa #000 #000 #aaa; background: #BBBBBB !important; }');
    AddGlobalStyle('.FollowerCheckerButton:hover, .FollowerCheckerButtonSmall:hover { border-color: #000 #aaa #aaa #000 !important; }');

    if (glblFollowersList == undefined) {
        Load();
    }

	CheckForUpdates();
    ConfigureEvents();
	ProcessContactsPage();
	Show();

	if (GM_getValue('running', '') && glblPageNumber < glblLastPage) {
		GM_setValue('missing', JSON.stringify(glblMissingFollowers));
		GM_setValue('actual', JSON.stringify(glblActualFollowers));
		GM_setValue('page', glblPageNumber);
		setTimeout(nextPage, 5000);
	}

	else {
		GM_setValue('running', '');
	}
}

Main();