VK/VKVideo: sort by time by ChatGPT 5 (v2.0 – fixed selectors)

Sorts visible videos by length (↑/↓) on VK Video channel pages

// ==UserScript==
// @name         VK/VKVideo: sort by time by ChatGPT 5 (v2.0 – fixed selectors) 
// @namespace    https://vkvideo.ru/
// @version      2.0
// @description  Sorts visible videos by length (↑/↓) on VK Video channel pages
// @match        *://vkvideo.ru/*
// @match        *://vk.com/*
// @icon         https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/VK.com-logo.svg/1200px-VK.com-logo.svg.png
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // Selectors from your page:
  const SEL = {
    LIST: '[data-testid="catalog_list_videos"]',
    ITEM: ':scope > [data-testid="grid-item"]',
    DURATION: '[data-testid="video_card_duration"]',
  };

  const MAX_Z = 2147483647;
  let ascending = true;

  const log = (...a) => console.log('[VK sort 2.0]', ...a);

  function timeToSeconds(t) {
    if (!t) return 0;
    const p = t.trim().split(':').map(Number);
    if (p.some(Number.isNaN)) return 0;
    if (p.length === 2) return p[0] * 60 + p[1];
    if (p.length === 3) return p[0] * 3600 + p[1] * 60 + p[2];
    return 0;
  }

  function toast(msg) {
    const t = document.createElement('div');
    t.textContent = msg;
    Object.assign(t.style, {
      position: 'fixed', bottom: '140px', right: '20px',
      zIndex: MAX_Z, padding: '10px 14px',
      background: '#2b2b2b', color: '#fff',
      borderRadius: '8px', boxShadow: '0 2px 8px rgba(0,0,0,.35)',
      fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
    });
    document.body.appendChild(t);
    setTimeout(() => t.remove(), 1600);
  }

  function findListAndItems() {
    const list = document.querySelector(SEL.LIST);
    if (!list) return { list: null, items: [] };
    const items = Array.from(list.querySelectorAll(SEL.ITEM));
    return { list, items };
  }

  function collect() {
    const { list, items } = findListAndItems();
    if (!list || !items.length) return { list: null, rows: [] };
    const rows = items.map(li => {
      const dur = li.querySelector(SEL.DURATION);
      return { li, seconds: timeToSeconds(dur?.textContent || ''), txt: dur?.textContent?.trim() || '' };
    });
    return { list, rows };
  }

  function sortNow() {
    const { list, rows } = collect();
    if (!list || !rows.length) {
      toast('Could not find the list/tiles. Scroll to load more and try again.');
      log('collect()', { list, rowsLen: rows.length });
      return;
    }

    rows.sort((a, b) => ascending ? a.seconds - b.seconds : b.seconds - a.seconds);

    const frag = document.createDocumentFragment();
    rows.forEach(r => frag.appendChild(r.li));
    list.appendChild(frag);

    toast(ascending ? 'Sorted ↑ (shorter → longer)' : 'Sorted ↓ (longer → shorter)');
    const btn = document.getElementById('vkvideo-sort-btn');
    if (btn) btn.textContent = ascending ? 'Sort by time ↓' : 'Sort by time ↑';
    ascending = !ascending;
  }

  function addButton() {
    if (document.getElementById('vkvideo-sort-btn')) return;
    const btn = document.createElement('button');
    btn.id = 'vkvideo-sort-btn';
    btn.textContent = 'Sort by time ↑';
    Object.assign(btn.style, {
      position: 'fixed', bottom: '200px', right: '20px', zIndex: MAX_Z,
      padding: '12px 18px', fontSize: '16px', lineHeight: '1',
      background: '#4a76a8', color: '#fff', border: 'none',
      borderRadius: '10px', cursor: 'pointer',
      boxShadow: '0 4px 12px rgba(0,0,0,.35)', pointerEvents: 'auto', userSelect: 'none',
    });
    btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); sortNow(); }, true);
    document.body.appendChild(btn);

    // shortcut: Alt+S
    window.addEventListener('keydown', e => {
      if (e.altKey && e.key.toLowerCase() === 's') { e.preventDefault(); sortNow(); }
    }, true);
  }

  function init() {
    addButton();
    // SPA: ensure the button remains when the DOM changes
    const obs = new MutationObserver(() => addButton());
    obs.observe(document.documentElement, { childList: true, subtree: true });
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
  else init();
})();