Merge branch 'main' of git@github.com:cdransf/coryd.dev.git

This commit is contained in:
Cory Dransfeldt 2024-05-07 17:29:46 -07:00
commit 0381408235
38 changed files with 504 additions and 14466 deletions

91
src/_data/artists.js Normal file
View file

@ -0,0 +1,91 @@
import { createClient } from '@supabase/supabase-js'
import { DateTime } from 'luxon'
const SUPABASE_URL = process.env.SUPABASE_URL
const SUPABASE_KEY = process.env.SUPABASE_KEY
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
const fetchDataForPeriod = async (startPeriod, fields, table, allTime = false) => {
let query = supabase.from(table).select(fields).order('listened_at', { ascending: false })
if (!allTime) query = query.gte('listened_at', startPeriod.toUTC().toSeconds())
const { data, error } = await query
if (error) {
console.error('Error fetching data:', error)
return []
}
return data
}
const aggregateData = (data, groupByField, groupByType) => {
const aggregation = {}
data.forEach(item => {
const key = item[groupByField]
if (!aggregation[key]) {
if (groupByType === 'track') {
aggregation[key] = {
title: item[groupByField],
plays: 0,
mbid: item['albums']?.mbid || '',
url: item['albums']?.mbid ? `https://musicbrainz.org/release/${item['albums'].mbid}` : `https://musicbrainz.org/search?query=${encodeURIComponent(item['album_name'])}&type=release`,
image: item['albums']?.image || '',
type: groupByType
}
} else {
aggregation[key] = {
title: item[groupByField],
plays: 0,
mbid: item[groupByType]?.mbid || '',
url: item[groupByType]?.mbid ? `https://musicbrainz.org/${groupByType === 'albums' ? 'release' : 'artist'}/${item[groupByType].mbid}` : `https://musicbrainz.org/search?query=${encodeURIComponent(item[groupByField])}&type=${groupByType === 'albums' ? 'release' : 'artist'}`,
image: item[groupByType]?.image || '',
type: groupByType
}
}
}
aggregation[key].plays++
})
return Object.values(aggregation).sort((a, b) => b.plays - a.plays)
}
export default async function() {
const periods = {
week: DateTime.now().minus({ days: 7 }).startOf('day'), // Last week
month: DateTime.now().minus({ days: 30 }).startOf('day'), // Last 30 days
threeMonth: DateTime.now().minus({ months: 3 }).startOf('day'), // Last three months
year: DateTime.now().minus({ years: 1 }).startOf('day'), // Last 365 days
allTime: null // Null indicates no start period constraint
}
const results = {}
const selectFields = `
track_name,
artist_name,
album_name,
album_key,
artists (mbid, image),
albums (mbid, image)
`
for (const [period, startPeriod] of Object.entries(periods)) {
const isAllTime = period === 'allTime'
const periodData = await fetchDataForPeriod(startPeriod, selectFields, 'listens', isAllTime)
results[period] = {
artists: aggregateData(periodData, 'artist_name', 'artists'),
albums: aggregateData(periodData, 'album_name', 'albums'),
tracks: aggregateData(periodData, 'track_name', 'track')
}
}
const recentData = await fetchDataForPeriod(DateTime.now().minus({ days: 7 }), selectFields, 'listens')
results.recent = {
artists: aggregateData(recentData, 'artist_name', 'artists'),
albums: aggregateData(recentData, 'album_name', 'albums'),
tracks: aggregateData(recentData, 'track_name', 'track')
}
return results
}

View file

@ -1,91 +0,0 @@
const sanitizeMediaString = (string) => string.normalize('NFD').replace(/[\u0300-\u036f\u2010—\.\?\(\)\[\]\{\}]/g, '').replace(/\.{3}/g, '')
const artistSanitizedKey = (artist) => `${sanitizeMediaString(artist).replace(/\s+/g, '-').toLowerCase()}`
const albumSanitizedKey = (artist, album) => `${sanitizeMediaString(artist).replace(/\s+/g, '-').toLowerCase()}-${sanitizeMediaString(album.replace(/[:\/\\,'']+/g
, '').replace(/\s+/g, '-').toLowerCase())}`
export const buildChart = (tracks, artists, albums, nowPlaying = {}) => {
const artistsData = {}
const albumsData = {}
const tracksData = {}
const objectToArraySorted = (inputObject) => Object.values(inputObject).sort((a, b) => b.plays - a.plays)
tracks.forEach(track => {
if (!tracksData[track['track']]) {
const artistKey = artistSanitizedKey(track['artist'])
tracksData[track['track']] = {
artist: track['artist'],
title: track['track'],
plays: 1,
type: 'track',
url: (artists[artistKey]?.['mbid'] && artists[artistKey]?.['mbid'] !== '') ? `http://musicbrainz.org/artist/${artists[artistKey]?.['mbid']}` : `https://musicbrainz.org/search?query=${track['artist'].replace(
/\s+/g,
'+'
)}&type=artist`,
}
} else {
tracksData[track['track']]['plays']++
}
if (!artistsData[track['artist']]) {
const artistKey = artistSanitizedKey(track['artist'])
artistsData[track['artist']] = {
title: track['artist'],
plays: 1,
mbid: artists[artistKey]?.['mbid'] || '',
url: (artists[artistKey]?.['mbid'] && artists[artistKey]?.['mbid'] !== '') ? `http://musicbrainz.org/artist/${artists[artistKey]?.['mbid']}` : `https://musicbrainz.org/search?query=${track['artist'].replace(
/\s+/g,
'+'
)}&type=artist`,
image: artists[artistSanitizedKey(track['artist'])]?.['image'] || `https://coryd.dev/media/artists/${sanitizeMediaString(track['artist']).replace(/\s+/g, '-').toLowerCase()}.jpg`,
type: 'artist'
}
} else {
artistsData[track['artist']]['plays']++
}
if (!albumsData[track['album']]) {
const albumKey = albumSanitizedKey(track['artist'], track['album'])
albumsData[track['album']] = {
title: track['album'],
artist: track['artist'],
plays: 1,
mbid: albums[albumKey]?.['mbid'] || '',
url: (albums[albumKey]?.['mbid'] && albums[albumSanitizedKey(track['artist'], track['artist'], track['album'])]?.['mbid'] !== '') ? `https://musicbrainz.org/release/${albums[albumKey]?.['mbid']}` : `https://musicbrainz.org/taglookup/index?tag-lookup.artist=${track['artist'].replace(/\s+/g, '+')}&tag-lookup.release=${track['album'].replace(/\s+/g, '+')}`,
image: albums[albumKey]?.['image'] || `https://coryd.dev/media/albums/${sanitizeMediaString(track['artist']).replace(/\s+/g, '-').toLowerCase()}-${sanitizeMediaString(track['album'].replace(/[:\/\\,'']+/g
, '').replace(/\s+/g, '-').toLowerCase())}.jpg`,
type: 'album'
}
} else {
albumsData[track['album']]['plays']++
}
})
const topTracks = objectToArraySorted(tracksData).splice(0, 10)
const topTracksData = {
data: topTracks,
mostPlayed: Math.max(...topTracks.map(track => track.plays))
}
return {
artists: objectToArraySorted(artistsData),
albums: objectToArraySorted(albumsData),
tracks: objectToArraySorted(tracksData),
topTracks: topTracksData,
nowPlaying
}
}
export const buildTracksWithArt = (tracks, artists, albums) => {
tracks.forEach(track => {
track['image'] = albums[albumSanitizedKey(track['artist'], track['album'])]?.['image'] || `https://coryd.dev/media/albums/${sanitizeMediaString(track['artist']).replace(/\s+/g, '-').toLowerCase()}-${sanitizeMediaString(track['album'].replace(/[:\/\\,'']+/g
, '').replace(/\s+/g, '-').toLowerCase())}.jpg`
track['url'] = (artists[artistSanitizedKey(track['artist'])]?.['mbid'] && artists[artistSanitizedKey(track['artist'])]?.['mbid'] !== '') ? `http://musicbrainz.org/artist/${artists[artistSanitizedKey(track['artist'])]?.['mbid']}` : `https://musicbrainz.org/search?query=${track['artist'].replace(
/\s+/g,
'+'
)}&type=artist`
})
return tracks
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
{"track":"Superior","album":"Lifeless Birth","artist":"Necrot","trackNumber":3,"timestamp":"2024-04-12T17:25:38.120+00:00","genre":"death metal","url":"http://musicbrainz.org/artist/0556f527-d02e-440c-b0bb-3e1aa402cf19"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,11 +1,110 @@
import { readFile } from 'fs/promises'
import { buildChart } from './helpers/music.js'
import { createClient } from '@supabase/supabase-js'
import { DateTime } from 'luxon'
export default async function () {
const window = JSON.parse(await readFile('./src/_data/json/scrobbles-window.json', 'utf8'));
const artists = JSON.parse(await readFile('./src/_data/json/artists-map.json', 'utf8'));
const albums = JSON.parse(await readFile('./src/_data/json/albums-map.json', 'utf8'));
const nowPlaying = JSON.parse(await readFile('./src/_data/json/now-playing.json', 'utf8'));
const SUPABASE_URL = process.env.SUPABASE_URL
const SUPABASE_KEY = process.env.SUPABASE_KEY
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
return buildChart(window['data'], artists, albums, nowPlaying)
const fetchDataForPeriod = async (startPeriod, fields, table) => {
const PAGE_SIZE = 1000
let rows = []
let rangeStart = 0
while (true) {
const { data, error } = await supabase
.from(table)
.select(fields)
.order('listened_at', { ascending: false })
.gte('listened_at', startPeriod.toSeconds())
.range(rangeStart, rangeStart + PAGE_SIZE - 1)
if (error) {
console.error(error)
break
}
rows = rows.concat(data)
if (data.length < PAGE_SIZE) break
rangeStart += PAGE_SIZE
}
return rows
}
const aggregateData = (data, groupByField, groupByType) => {
const aggregation = {}
data.forEach(item => {
const key = item[groupByField]
if (!aggregation[key]) {
if (groupByType === 'track') {
aggregation[key] = {
title: item[groupByField],
plays: 0,
mbid: item['albums']?.mbid || '',
url: item['albums']?.mbid ? `https://musicbrainz.org/release/${item['albums'].mbid}` : `https://musicbrainz.org/search?query=${encodeURIComponent(item['album_name'])}&type=release`,
image: item['albums']?.image || '',
timestamp: item['listened_at'],
type: groupByType
}
} else {
aggregation[key] = {
title: item[groupByField],
plays: 0,
mbid: item[groupByType]?.mbid || '',
url: item[groupByType]?.mbid ? `https://musicbrainz.org/${groupByType === 'albums' ? 'release' : 'artist'}/${item[groupByType].mbid}` : `https://musicbrainz.org/search?query=${encodeURIComponent(item[groupByField])}&type=${groupByType === 'albums' ? 'release' : 'artist'}`,
image: item[groupByType]?.image || '',
type: groupByType
}
}
if (
groupByType === 'track' ||
groupByType === 'albums'
) aggregation[key]['artist'] = item['artist_name']
}
aggregation[key].plays++
})
return Object.values(aggregation).sort((a, b) => b.plays - a.plays)
}
export default async function() {
const periods = {
week: DateTime.now().minus({ days: 7 }).startOf('day'), // Last week
month: DateTime.now().minus({ days: 30 }).startOf('day'), // Last 30 days
threeMonth: DateTime.now().minus({ months: 3 }).startOf('day'), // Last three months
year: DateTime.now().minus({ years: 1 }).startOf('day'), // Last 365 days
}
const results = {}
const selectFields = `
track_name,
artist_name,
album_name,
album_key,
listened_at,
artists (mbid, image),
albums (mbid, image)
`
for (const [period, startPeriod] of Object.entries(periods)) {
const periodData = await fetchDataForPeriod(startPeriod, selectFields, 'listens')
results[period] = {
artists: aggregateData(periodData, 'artist_name', 'artists'),
albums: aggregateData(periodData, 'album_name', 'albums'),
tracks: aggregateData(periodData, 'track_name', 'track')
}
}
const recentData = await fetchDataForPeriod(DateTime.now().minus({ days: 7 }), selectFields, 'listens')
results.recent = {
artists: aggregateData(recentData, 'artist_name', 'artists'),
albums: aggregateData(recentData, 'album_name', 'albums'),
tracks: aggregateData(recentData, 'track_name', 'track')
}
results.nowPlaying = results.recent.tracks[0]
return results
}

View file

@ -1,18 +0,0 @@
import { readFile } from 'fs/promises'
import { buildChart, buildTracksWithArt } from './helpers/music.js'
export default async function () {
const monthChart = JSON.parse(await readFile('./src/_data/json/scrobbles-month-chart.json', 'utf8'));
const threeMonthChart = JSON.parse(await readFile('./src/_data/json/scrobbles-three-month-chart.json', 'utf8'));
const yearChart = JSON.parse(await readFile('./src/_data/json/scrobbles-year-chart.json', 'utf8'));
const artists = JSON.parse(await readFile('./src/_data/json/artists-map.json', 'utf8'));
const albums = JSON.parse(await readFile('./src/_data/json/albums-map.json', 'utf8'));
const recent = JSON.parse(await readFile('./src/_data/json/scrobbles-window.json', 'utf8'))['data'].reverse().splice(0,10)
return {
recent: buildTracksWithArt(recent, artists, albums),
month: buildChart(monthChart['data'], artists, albums),
threeMonth: buildChart(threeMonthChart['data'], artists, albums),
year: buildChart(yearChart['data'], artists, albums),
}
}

View file

@ -1,25 +0,0 @@
import { readFile } from 'fs/promises'
import { buildChart } from './helpers/music.js'
import { DateTime } from 'luxon'
export default async function () {
const currentDate = DateTime.now()
const lastWeek = currentDate.minus({ weeks: 1 })
const artists = JSON.parse(await readFile('./src/_data/json/artists-map.json', 'utf8'));
const albums = JSON.parse(await readFile('./src/_data/json/albums-map.json', 'utf8'));
const chartData = JSON.parse(await readFile('./src/_data/json/weekly-top-artists-chart.json', 'utf8'))
const artistChart = buildChart(chartData['data'], artists, albums)['artists'].splice(0, 8)
let content = 'My top artists for the week: '
artistChart.forEach((artist, index) => {
content += `${artist['title']} @ ${artist['plays']} play${parseInt(artist['plays']) > 1 ? 's' : ''}`
if (index !== artistChart.length - 1) content += ', '
})
content += ' #Music'
return [{
title: content,
url: `https://coryd.dev/now?ts=${lastWeek.year}-${lastWeek.weekNumber}#artists`,
date: DateTime.fromMillis(parseInt(chartData['timestamp'])).toISO(),
description: `My top artists for the last week.<br/><br/>`
}]
}

View file

@ -19,16 +19,16 @@ layout: default
</div>
</div>
<div id="artists-window">
{% render "partials/now/media-grid.liquid", data:music.artists, shape: "square", count: 8, loading: "eager" %}
{% render "partials/now/media-grid.liquid", data:music.week.artists, shape: "square", count: 8, loading: "eager" %}
</div>
<div class="hidden" id="artists-month">
{% render "partials/now/media-grid.liquid", data:musicCharts.month.artists, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.month.artists, shape: "square", count: 8 %}
</div>
<div class="hidden" id="artists-three-months">
{% render "partials/now/media-grid.liquid", data:musicCharts.threeMonth.artists, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.threeMonth.artists, shape: "square", count: 8 %}
</div>
<div class="hidden" id="artists-year">
{% render "partials/now/media-grid.liquid", data:musicCharts.year.artists, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.year.artists, shape: "square", count: 8 %}
</div>
<div class="section-header-wrapper">
<h2 id="albums" class="section-header flex-centered">
@ -43,16 +43,16 @@ layout: default
</div>
</div>
<div id="albums-window">
{% render "partials/now/media-grid.liquid", data:music.albums, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.week.albums, shape: "square", count: 8 %}
</div>
<div class="hidden" id="albums-month">
{% render "partials/now/media-grid.liquid", data:musicCharts.month.albums, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.month.albums, shape: "square", count: 8 %}
</div>
<div class="hidden" id="albums-three-months">
{% render "partials/now/media-grid.liquid", data:musicCharts.threeMonth.albums, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.threeMonth.albums, shape: "square", count: 8 %}
</div>
<div class="hidden" id="albums-year">
{% render "partials/now/media-grid.liquid", data:musicCharts.year.albums, shape: "square", count: 8 %}
{% render "partials/now/media-grid.liquid", data:music.year.albums, shape: "square", count: 8 %}
</div>
<div class="section-header-wrapper">
<h2 id="tracks" class="section-header flex-centered">
@ -68,21 +68,21 @@ layout: default
</div>
</div>
<div id="tracks-recent">
{% render "partials/now/tracks-recent.liquid", data:musicCharts.recent %}
{% render "partials/now/tracks-recent.liquid", data:music.recent.tracks %}
</div>
<div class="hidden" id="tracks-window">
{% render "partials/now/track-chart.liquid", data:music.topTracks.data, mostPlayed:music.topTracks.mostPlayed %}
{% render "partials/now/track-chart.liquid", data:music.week.tracks, mostPlayed:music.week.tracks[0].plays %}
</div>
<div class="hidden" id="tracks-month">
{% render "partials/now/track-chart.liquid", data:musicCharts.month.topTracks.data, mostPlayed:musicCharts.month.topTracks.mostPlayed %}
{% render "partials/now/track-chart.liquid", data:music.month.tracks, mostPlayed:music.month.tracks[0].plays %}
</div>
<div class="hidden" id="tracks-three-months">
{% render "partials/now/track-chart.liquid", data:musicCharts.threeMonth.topTracks.data, mostPlayed:musicCharts.threeMonth.topTracks.mostPlayed %}
{% render "partials/now/track-chart.liquid", data:music.threeMonth.tracks, mostPlayed:music.threeMonth.tracks[0].plays %}
</div>
<div class="hidden" id="tracks-year">
{% render "partials/now/track-chart.liquid", data:musicCharts.year.topTracks.data, mostPlayed:musicCharts.year.topTracks.mostPlayed %}
{% render "partials/now/track-chart.liquid", data:music.year.tracks, mostPlayed:music.year.tracks[0].plays %}
</div>
{% render "partials/now/albumReleases.liquid", albumReleases:albumReleases %}
{% render "partials/now/album-releases.liquid", albumReleases:albumReleases %}
<h2 id="books" class="section-header flex-centered">
{% tablericon "books" "Books" %}
Books

View file

@ -1,12 +1,12 @@
{% if data.size > 0 %}
<div class="music-chart">
{% for item in data limit: 10 %}
{% capture alt %}{{ item.track | escape }} by {{ item.artist }}{% endcapture %}
{% capture alt %}{{ item.title | escape }} by {{ item.artist }}{% endcapture %}
<div class="item">
<div class="meta">
<img src="https://coryd.dev/.netlify/images/?url={{ item.image }}&fit=cover&w=64&h=64&fm=webp&q=65" class="image-banner" alt="{{ alt }}" loading="lazy" decoding="async" width="64" height="64" />
<div class="meta-text">
<div class="title">{{ item.track }}</div>
<div class="title">{{ item.title }}</div>
<div class="subtext">
<a href="{{ item.url }}">{{ item.artist }}</a>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 0 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -201,6 +201,7 @@ hr {
table {
display: block;
overflow-x: scroll;
overscroll-behavior: contain;
width: 100%;
max-width: fit-content;
margin: 0 auto;

View file

@ -1,13 +0,0 @@
---
layout: null
eleventyExcludeFromCollections: true
permalink: /feeds/weekly-artist-chart
---
{% render "partials/feeds/rss.liquid"
permalink:"/feeds/weekly-artist-chart"
title:"Weekly artist chart • Cory Dransfeldt"
description:"The top 8 artists I've listened to this week."
data:weeklyArtistChart
updated:weeklyArtistChart[0].date
site:site
%}

View file

@ -1,7 +1,7 @@
---
layout: default
---
{% render "partials/home/now.liquid" status:status, artists:music.artists, books:books, tv:tv %}
{% render "partials/home/now.liquid" status:status, artists:music.week.artists, books:books, tv:tv %}
{% render "partials/home/posts.liquid" icon: "star", title: "Featured", postData:collections.posts, postType: "featured" %}
{% assign posts = collections.posts | reverse %}
{% render "partials/home/posts.liquid" icon: "clock-2", title: "Recent posts", postData:posts %}

View file

@ -3,7 +3,7 @@ title: About
layout: default
permalink: /about.html
---
{%- assign artist = music.artists | first -%}
{%- assign artist = music.week.artists | first -%}
{%- assign book = books | bookStatus: 'started' | reverse | first -%}
{%- assign show = tv | first -%}
<div class="avatar-wrapper flex-centered">

View file

@ -20,7 +20,7 @@ description: "See what I'm doing now."
</p>
<p>
{% tablericon "headphones" "Listening to" %}
Listening to tracks like <strong class="highlight-text">{{ music.nowPlaying.track }}</strong> by <strong class="highlight-text">{{ music.nowPlaying.artist }}</strong>.
Listening to tracks like <strong class="highlight-text">{{ music.nowPlaying.title }}</strong> by <strong class="highlight-text">{{ music.nowPlaying.artist }}</strong>.
</p>
<p>
{% tablericon "needle" "Getting tattooed" %}

View file

@ -136,7 +136,7 @@ layout: main
{{ content }}
{% render "partials/now/media-grid.liquid", data:artists, icon: "microphone-2", title: "Artists", shape: "square", count: 8, loading: 'eager' %}
{% render "partials/now/media-grid.liquid", data:albums, icon: "vinyl", title: "Albums", shape: "square", count: 8, loading: 'lazy' %}
{% render "partials/now/albumReleases.liquid", albumReleases:albumReleases %}
{% render "partials/now/album-releases.liquid", albumReleases:albumReleases %}
{% render "partials/now/media-grid.liquid", data:books, icon: "books", title: "Books", shape: "vertical", count: 6, loading: 'lazy' %}
{% render "partials/now/links.liquid", links:links %}
{% render "partials/now/media-grid.liquid", data:movies, icon: "movie", title: "Movies", shape: "vertical", count: 6, loading: 'lazy' %}

View file

@ -0,0 +1,38 @@
---
date: 2024-05-06T11:14-08:00
title: The tech industry doesn't deserve optimism it has earned skepticism
description: "Take a step back look around at the tech products you use, the industry and its impact on society more broadly and ask yourself: does its track record warrant optimism or have they earned a healthy degree of skepticism?"
tags:
- tech
- AI
- 'social media'
---
Take a step back look around at the tech products you use, the industry and its impact on society more broadly and ask yourself: does its track record warrant optimism or have they earned a healthy degree of skepticism?<!-- excerpt -->
The web started out premised on and promising open connection. It delivered on that early promise when it was in its nascent form but the commercialization of the web in the form of early gatekeepers rapidly closed that off.
We started with protocols and were then herded into platforms that offered convenience. When they grew large enough, that convenience gave way to captivity.
We were promised a mobile revolution and greater connectivity as the smartphone era took hold and companies — initially — delivered on that promise. Video calls became ubiquitous, social media platforms grew their reach and landed in our pockets, we were able to capture video and photos in an instant.
Those social media companies, again, offered convenience and an — as well know — to good to be true promise of free and open access. We closed our blogs, got in line and ceded control of our social graphs. Drawbridges were rolled up, ads increased and nobody left — at least not in droves. Everyone is there so everyone stays.
Journalists and newspapers were drawn in, promised an audience and were gifted with capricious intermediaries that destroyed the profession and industry.
We lost our handle on what is and was true, stopped having conversations and started yelling at their representations. It became easier to shout at someone on line than it was to have a healthier discourse.
They took jobs that once represented viable professions, routed them through apps, took away benefits and stability, steamrolled regulators, operated services at a loss and captured users. Now we're raising prices, but not to the benefit of anyone but the tech-enabled middlemen.
They moved TV and movies to the internet, promised a fair price and flexible access to broad content catalogs. Now we're consolidating, raising prices and content keeps disappearing.
They offered musicians easier distribution and larger audiences while users' curated music collections faded away. Now we're raising prices, slashing payouts to artists and treating music as more and more of an afterthought.
We traded local businesses for massive ecommerce retailers. Those retailers promised convenience and precision delivery — they provided that, but at the cost of backbreaking labor and precarious employment for delivery drivers.
They promised an electric vehicle and transportation revolution. We delivered premium electric vehicles, grew with the help of subsidies, scrapped plans for affordable vehicles and kept over-promising while conveniently moving the goal posts.
They promised decentralized finance, ignored the energy and environmental costs, re-opened fossil-fueled power plants and failed to deliver much more than a series of high profile scandals and regulatory interventions. Instead of a new medium of exchange, we got volatile speculation and grift.
Now they're promising AI and ignoring yet more collateral damage. We're throwing piles of cache at GPUs, hardware and data centers. We're using increasingly large volumes of water. We're asserting the right to any and all data we can access. All of this while we're providing minimal productivity increases or value at scale.
The tech industry has made a lot of problems, it's delivered for company owners and shareholders but, as with so many things, they often externalize and downplay the harms. They call for more optimism and will gladly push a shiny new toy, app, nonsensical vision for a privatized utopia — you name it — but what they deliver, in reality, is very far removed from that vision. <strong class="highlight-text">They aren't entitled to optimism — they've earned skepticism.</strong>