App Store Metadata Viewer

Displays bundle ID & full metadata on App Store pages with advanced features, copy/export buttons, and improved UI

  1. // ==UserScript==
  2. // @name App Store Metadata Viewer
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.3
  5. // @description Displays bundle ID & full metadata on App Store pages with advanced features, copy/export buttons, and improved UI
  6. // @author sharmanhall
  7. // @match https://apps.apple.com/*/app/*/id*
  8. // @match https://apps.apple.com/app/id*
  9. // @grant GM_setClipboard
  10. // @grant GM_download
  11. // @grant GM_registerMenuCommand
  12. // @license MIT
  13. // @icon https://www.google.com/s2/favicons?sz=64&domain=apple.com
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. const settings = {
  20. displayMode: 'overlay',
  21. autoCopy: false
  22. };
  23.  
  24. GM_registerMenuCommand('Toggle Display Mode', () => {
  25. settings.displayMode = settings.displayMode === 'overlay' ? 'inline' : 'overlay';
  26. alert(`Switched to: ${settings.displayMode}`);
  27. location.reload();
  28. });
  29.  
  30. GM_registerMenuCommand('Toggle Auto-Copy', () => {
  31. settings.autoCopy = !settings.autoCopy;
  32. alert(`Auto-copy is now ${settings.autoCopy ? 'enabled' : 'disabled'}`);
  33. });
  34.  
  35. function waitForAppId(attempt = 0) {
  36. const appIdMatch = window.location.href.match(/id(\d+)/);
  37. if (!appIdMatch && attempt < 10) {
  38. return setTimeout(() => waitForAppId(attempt + 1), 1000);
  39. } else if (!appIdMatch) {
  40. console.error('[Metadata Viewer] App ID not found.');
  41. return;
  42. }
  43. fetchAppData(appIdMatch[1]);
  44. }
  45.  
  46. async function fetchAppData(appId) {
  47. const lookupUrl = `https://itunes.apple.com/lookup?id=${appId}`;
  48. try {
  49. const res = await fetch(lookupUrl);
  50. const data = await res.json();
  51. if (!data.results || !data.results.length) throw 'No results';
  52. showInfo(data.results[0]);
  53. } catch (err) {
  54. console.error('[Metadata Viewer] Failed to fetch metadata:', err);
  55. }
  56. }
  57.  
  58. function formatBytes(bytes) {
  59. const kb = bytes / 1024;
  60. if (kb < 1024) return `${Math.round(kb)} KB`;
  61. return `${(kb / 1024).toFixed(1)} MB`;
  62. }
  63.  
  64. function showInfo(app) {
  65. const {
  66. bundleId,
  67. version,
  68. minimumOsVersion,
  69. releaseDate,
  70. primaryGenreName,
  71. languageCodesISO2A,
  72. fileSizeBytes,
  73. sellerName,
  74. trackViewUrl
  75. } = app;
  76.  
  77. const country = new URL(window.location.href).pathname.split('/')[1].toUpperCase();
  78. const lang = languageCodesISO2A?.join(', ') || 'N/A';
  79.  
  80. const infoHTML = `
  81. <div id="bundle-id-widget" style="font-family: 'Segoe UI', Roboto, monospace; font-size: 13px; line-height: 1.8;">
  82. <div style="margin-bottom: 8px;"><strong>📦 Bundle ID:</strong> ${bundleId}</div>
  83. <div><strong>👨‍💻 Developer:</strong> ${sellerName}</div>
  84. <div><strong>📱 Version:</strong> ${version}</div>
  85. <div><strong>📅 Release Date:</strong> ${releaseDate?.split('T')[0]}</div>
  86. <div><strong>📂 Size:</strong> ${formatBytes(fileSizeBytes)}</div>
  87. <div><strong>🧭 Min OS:</strong> ${minimumOsVersion || 'N/A'}</div>
  88. <div><strong>🗂 Genre:</strong> ${primaryGenreName}</div>
  89. <div><strong>🌍 Country:</strong> ${country}</div>
  90. <div style="margin-bottom: 12px;"><strong>🗣️ Language(s):</strong> ${lang}</div>
  91.  
  92. <div style="display: flex; gap: 10px; flex-wrap: wrap;">
  93. <button id="copyBtn" style="background:#1E88E5;color:white;border:none;padding:6px 10px;border-radius:5px;cursor:pointer;">📋 Copy Bundle ID</button>
  94. <button id="jsonBtn" style="background:#333;color:white;border:none;padding:6px 10px;border-radius:5px;cursor:pointer;">🗄 Export JSON</button>
  95. <a href="${trackViewUrl}" target="_blank" style="color:#42A5F5;text-decoration:none;padding:6px 10px;border-radius:5px;background:#222;display:inline-block;">🔗 View on App Store</a>
  96. </div>
  97. </div>
  98. `;
  99.  
  100. const container = document.createElement('div');
  101. container.innerHTML = infoHTML;
  102.  
  103. if (settings.displayMode === 'inline') {
  104. container.style.background = '#f9f9f9';
  105. container.style.padding = '14px';
  106. container.style.border = '1px solid #ccc';
  107. container.style.borderRadius = '8px';
  108. container.style.marginTop = '20px';
  109. const target = document.querySelector('h1') || document.body;
  110. target.parentElement.insertBefore(container, target.nextSibling);
  111. } else {
  112. Object.assign(container.style, {
  113. position: 'fixed',
  114. bottom: '20px',
  115. right: '20px',
  116. background: '#111',
  117. color: '#fff',
  118. padding: '16px',
  119. borderRadius: '10px',
  120. boxShadow: '0 0 16px rgba(0,0,0,0.6)',
  121. zIndex: 99999,
  122. maxWidth: '300px',
  123. opacity: '85%'
  124. });
  125. document.body.appendChild(container);
  126. }
  127.  
  128. const copyBtn = document.getElementById('copyBtn');
  129. copyBtn.onclick = () => {
  130. (typeof GM_setClipboard === 'function' ? GM_setClipboard : navigator.clipboard.writeText)(bundleId);
  131. copyBtn.textContent = '✅ Copied!';
  132. setTimeout(() => (copyBtn.textContent = '📋 Copy Bundle ID'), 1500);
  133. };
  134.  
  135. const jsonBtn = document.getElementById('jsonBtn');
  136. jsonBtn.onclick = () => {
  137. const blob = new Blob([JSON.stringify(app, null, 2)], { type: 'application/json' });
  138. const url = URL.createObjectURL(blob);
  139. GM_download({ url, name: `bundleinfo-${bundleId}.json` });
  140. };
  141.  
  142. if (settings.autoCopy) {
  143. (typeof GM_setClipboard === 'function' ? GM_setClipboard : navigator.clipboard.writeText)(bundleId);
  144. }
  145. }
  146.  
  147. window.addEventListener('load', waitForAppId);
  148. })();