Clean up data files and fix icon/PDF links
- Clean all JSON data files: convert MongoDB format to clean JSON - works.json, images.json, publications.json, pubs.json, talks.json - releases.json, album_art.json, scores.json, my_image_gallery.json - events.json (with legacy program → program transformation) - resume.json (simplified structure) - Simplify all API routes (remove cleanData functions) - Fix PDF links to open in new tab (scores, writings, albums) - Upgrade to Nuxt 4.3.1 and fix carousel (nuxt-swiper) - Replace nuxt-icon with @nuxt/icon - Fix IconButton component for new tab links - Update cv.vue for resume data structure changes - Add icon collections (@iconify-json packages)
This commit is contained in:
parent
61332c28ef
commit
711b5b93c2
|
|
@ -58,4 +58,4 @@
|
||||||
export default {
|
export default {
|
||||||
props: ['upcoming_events']
|
props: ['upcoming_events']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,22 @@
|
||||||
<div class="inline-flex p-1 min-w-[25px]">
|
<div class="inline-flex p-1 min-w-[25px]">
|
||||||
<div v-show="visible" class="bg-black rounded-full text-xs inline-flex" >
|
<div v-show="visible" class="bg-black rounded-full text-xs inline-flex" >
|
||||||
|
|
||||||
<NuxtLink v-if="type === 'score'" :to="link" class="inline-flex p-1">
|
<NuxtLink v-if="type === 'score' && !newTab" :to="link" class="inline-flex p-1">
|
||||||
<Icon name="ion:book-sharp" style="color: white" />
|
<Icon name="ion:book-sharp" style="color: white" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<NuxtLink v-else-if="type === 'document'" class="inline-flex p-1" :to="link" :target="newTab ? '_blank' : undefined">
|
<a v-else-if="type === 'score' && newTab" :href="link" class="inline-flex p-1" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Icon name="ion:book-sharp" style="color: white" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<NuxtLink v-else-if="type === 'document' && !newTab" class="inline-flex p-1" :to="link">
|
||||||
<Icon name="ion:book-sharp" style="color: white" />
|
<Icon name="ion:book-sharp" style="color: white" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
|
<a v-else-if="type === 'document' && newTab" :href="link" class="inline-flex p-1" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Icon name="ion:book-sharp" style="color: white" />
|
||||||
|
</a>
|
||||||
|
|
||||||
<NuxtLink v-else-if="type === 'buy'" class="inline-flex p-1" :to="link">
|
<NuxtLink v-else-if="type === 'buy'" class="inline-flex p-1" :to="link">
|
||||||
<Icon name="bxs:purchase-tag" style="color: white" />
|
<Icon name="bxs:purchase-tag" style="color: white" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
@ -51,4 +59,4 @@
|
||||||
export default {
|
export default {
|
||||||
props: ['type', 'work', 'visible', 'link', 'newTab']
|
props: ['type', 'work', 'visible', 'link', 'newTab']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<SwiperSlide v-for="image in gallery" class="!flex !items-center !justify-center !h-auto !py-10 !bg-zinc-100">
|
<SwiperSlide v-for="image in gallery" class="!flex !items-center !justify-center !h-auto !py-10 !bg-zinc-100">
|
||||||
<img :src="'/' + bucket + '/' + image.image"
|
<img :src="'/' + bucket + '/' + image.image"
|
||||||
style="max-width: 100%; max-height: 70vh; object-fit: contain;"/>
|
style="max-width: calc(100% - 80px); max-height: 70vh; object-fit: contain;"/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
</Swiper>
|
</Swiper>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -35,4 +35,4 @@
|
||||||
export default {
|
export default {
|
||||||
props: ['gallery', 'bucket']
|
props: ['gallery', 'bucket']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
21897
package-lock.json
generated
21897
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -27,7 +27,7 @@
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"mongodb": "^7.1.0",
|
"mongodb": "^7.1.0",
|
||||||
"nuxt": "^4.3.1",
|
"nuxt": "^4.3.1",
|
||||||
"nuxt-swiper": "^2.0.1",
|
"nuxt-swiper": "^1.2.2",
|
||||||
"nuxt-umami": "^3.2.1",
|
"nuxt-umami": "^3.2.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5"
|
||||||
|
|
|
||||||
|
|
@ -23,34 +23,6 @@
|
||||||
For the Lecture-Concert on 22 Nov 2023, Registration recommended. Sign up <NuxtLink class="text-2xl font-bold" to='https://www.eventbrite.de/e/a-history-of-the-domino-problem-lecture-concert-tickets-707700981687'>HERE</NuxtLink>.
|
For the Lecture-Concert on 22 Nov 2023, Registration recommended. Sign up <NuxtLink class="text-2xl font-bold" to='https://www.eventbrite.de/e/a-history-of-the-domino-problem-lecture-concert-tickets-707700981687'>HERE</NuxtLink>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!---
|
|
||||||
<Swiper
|
|
||||||
:loop="true"
|
|
||||||
:spaceBetween="30"
|
|
||||||
:centeredSlides="true"
|
|
||||||
:pagination='{
|
|
||||||
clickable: true,
|
|
||||||
renderBullet: function (index, className) {
|
|
||||||
return "<span class=" + className + ">" + ["about", "exhibition", "events", "participants"][index] + "</span>";
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
:navigation="false"
|
|
||||||
:style="{
|
|
||||||
'--swiper-pagination-color': 'rgb(255 255 255)',
|
|
||||||
'--swiper-pagination-bullet-horizontal-gap': '80px',
|
|
||||||
'--swiper-pagination-bullet-inactive-opacity': '0.8',
|
|
||||||
'--swiper-pagination-bullet-size': '0px',
|
|
||||||
'--swiper-pagination-bottom': 'auto',
|
|
||||||
'--swiper-pagination-top': '1rem',
|
|
||||||
'--swiper-pagination-margin-left': '0px',
|
|
||||||
}"
|
|
||||||
:hashNavigation="{
|
|
||||||
watchState: true,
|
|
||||||
}"
|
|
||||||
:modules="[SwiperAutoplay, SwiperPagination, SwiperNavigation, SwiperHashNavigation]"
|
|
||||||
class="max-w-[95vw]"
|
|
||||||
>
|
|
||||||
--->
|
|
||||||
<Swiper
|
<Swiper
|
||||||
:loop="true"
|
:loop="true"
|
||||||
:spaceBetween="30"
|
:spaceBetween="30"
|
||||||
|
|
@ -327,20 +299,11 @@
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import hdp_background from "assets/hdp_background.png";
|
import hdp_background from "assets/hdp_background.png"
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
image: hdp_background, };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
|
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
|
||||||
|
|
||||||
|
const image = hdp_background
|
||||||
const audioPlayerStore = useAudioPlayerStore()
|
const audioPlayerStore = useAudioPlayerStore()
|
||||||
audioPlayerStore.setSoundCloudTrackID(324252345)
|
audioPlayerStore.setSoundCloudTrackID(324252345)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
18
pages/cv.vue
18
pages/cv.vue
|
|
@ -5,7 +5,7 @@ definePageMeta({
|
||||||
|
|
||||||
const { data: resumeData } = await useFetch('/api/resume')
|
const { data: resumeData } = await useFetch('/api/resume')
|
||||||
const { data: talksData } = await useFetch('/api/talks')
|
const { data: talksData } = await useFetch('/api/talks')
|
||||||
const resume = computed(() => resumeData.value?.[0])
|
const resume = computed(() => resumeData.value)
|
||||||
|
|
||||||
const talksByYear = computed(() => {
|
const talksByYear = computed(() => {
|
||||||
if (!talksData.value) return []
|
if (!talksData.value) return []
|
||||||
|
|
@ -74,7 +74,7 @@ useHead({
|
||||||
<section v-if="resume?.education?.length" class="cv-section">
|
<section v-if="resume?.education?.length" class="cv-section">
|
||||||
<h4>Education</h4>
|
<h4>Education</h4>
|
||||||
<div class="cv-entry">
|
<div class="cv-entry">
|
||||||
<div v-for="edu in resume.education" :key="edu.id" class="item">
|
<div v-for="(edu, idx) in resume.education" :key="idx" class="item">
|
||||||
<span class="item-title">{{ edu.studyType }} in {{ edu.area }}</span>
|
<span class="item-title">{{ edu.studyType }} in {{ edu.area }}</span>
|
||||||
<span class="item-meta">{{ edu.institution }}, {{ formatYear(edu.endDate) }}</span>
|
<span class="item-meta">{{ edu.institution }}, {{ formatYear(edu.endDate) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -85,7 +85,7 @@ useHead({
|
||||||
<section v-if="resume?.teaching?.length" class="cv-section">
|
<section v-if="resume?.teaching?.length" class="cv-section">
|
||||||
<h4>Teaching</h4>
|
<h4>Teaching</h4>
|
||||||
<div class="cv-entry">
|
<div class="cv-entry">
|
||||||
<div v-for="teach in resume.teaching" :key="teach.id" class="item">
|
<div v-for="(teach, idx) in resume.teaching" :key="idx" class="item">
|
||||||
<div class="item-header">
|
<div class="item-header">
|
||||||
<span class="item-title">{{ teach.company }}</span>
|
<span class="item-title">{{ teach.company }}</span>
|
||||||
<span class="item-subtitle">{{ teach.position }}</span>
|
<span class="item-subtitle">{{ teach.position }}</span>
|
||||||
|
|
@ -121,7 +121,7 @@ useHead({
|
||||||
<section v-if="resume?.work?.length" class="cv-section">
|
<section v-if="resume?.work?.length" class="cv-section">
|
||||||
<h4>Relevant Work</h4>
|
<h4>Relevant Work</h4>
|
||||||
<div class="cv-entry">
|
<div class="cv-entry">
|
||||||
<div v-for="w in resume.work" :key="w.id" class="item">
|
<div v-for="(w, idx) in resume.work" :key="idx" class="item">
|
||||||
<div class="item-header">
|
<div class="item-header">
|
||||||
<span class="item-title">{{ w.company }}</span>
|
<span class="item-title">{{ w.company }}</span>
|
||||||
<span class="item-subtitle">{{ w.position }}</span>
|
<span class="item-subtitle">{{ w.position }}</span>
|
||||||
|
|
@ -138,7 +138,7 @@ useHead({
|
||||||
<section v-if="resume?.skills?.length" class="cv-section">
|
<section v-if="resume?.skills?.length" class="cv-section">
|
||||||
<h4>Coding Skills</h4>
|
<h4>Coding Skills</h4>
|
||||||
<p class="item-detail">
|
<p class="item-detail">
|
||||||
<span v-for="(skill, idx) in resume.skills" :key="skill.id">
|
<span v-for="(skill, idx) in resume.skills" :key="idx">
|
||||||
{{ skill.keywords?.join(', ') }}
|
{{ skill.keywords?.join(', ') }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -148,7 +148,7 @@ useHead({
|
||||||
<section v-if="resume?.languages?.length" class="cv-section">
|
<section v-if="resume?.languages?.length" class="cv-section">
|
||||||
<h4>Language Skills</h4>
|
<h4>Language Skills</h4>
|
||||||
<p class="item-detail">
|
<p class="item-detail">
|
||||||
<span v-for="(lang, idx) in resume.languages" :key="lang.id">
|
<span v-for="(lang, idx) in resume.languages" :key="idx">
|
||||||
{{ lang.language }} — {{ lang.fluency }}{{ idx < resume.languages.length - 1 ? '; ' : '' }}
|
{{ lang.language }} — {{ lang.fluency }}{{ idx < resume.languages.length - 1 ? '; ' : '' }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -178,7 +178,7 @@ useHead({
|
||||||
|
|
||||||
<div v-if="resume?.solo_releases?.length" class="subsection">
|
<div v-if="resume?.solo_releases?.length" class="subsection">
|
||||||
<div class="subsection-title"><strong>Solo Albums</strong></div>
|
<div class="subsection-title"><strong>Solo Albums</strong></div>
|
||||||
<div v-for="rel in resume.solo_releases" :key="rel.id" class="item recording-item">
|
<div v-for="(rel, idx) in resume.solo_releases" :key="idx" class="item recording-item">
|
||||||
<span class="item-title">{{ rel.title }}</span>
|
<span class="item-title">{{ rel.title }}</span>
|
||||||
<span class="item-detail">{{ rel.publisher }}. {{ rel.media_type }}. {{ rel.date }}.</span>
|
<span class="item-detail">{{ rel.publisher }}. {{ rel.media_type }}. {{ rel.date }}.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -186,7 +186,7 @@ useHead({
|
||||||
|
|
||||||
<div v-if="resume?.compilation_releases?.length" class="subsection">
|
<div v-if="resume?.compilation_releases?.length" class="subsection">
|
||||||
<div class="subsection-title"><strong>Compilation Albums</strong></div>
|
<div class="subsection-title"><strong>Compilation Albums</strong></div>
|
||||||
<div v-for="rel in resume.compilation_releases" :key="rel.id" class="item recording-item">
|
<div v-for="(rel, idx) in resume.compilation_releases" :key="idx" class="item recording-item">
|
||||||
<span class="item-title">{{ rel.title }}</span>
|
<span class="item-title">{{ rel.title }}</span>
|
||||||
<span class="item-detail">{{ rel.publisher }}. {{ rel.media_type }}. {{ rel.date }}.</span>
|
<span class="item-detail">{{ rel.publisher }}. {{ rel.media_type }}. {{ rel.date }}.</span>
|
||||||
<div class="item-detail">featuring <span class="italic">{{ rel.work }}</span></div>
|
<div class="item-detail">featuring <span class="italic">{{ rel.work }}</span></div>
|
||||||
|
|
@ -198,7 +198,7 @@ useHead({
|
||||||
<section v-if="resume?.residencies?.length" class="cv-section">
|
<section v-if="resume?.residencies?.length" class="cv-section">
|
||||||
<h4>Residencies and Awards</h4>
|
<h4>Residencies and Awards</h4>
|
||||||
<div class="cv-entry">
|
<div class="cv-entry">
|
||||||
<div v-for="res in resume.residencies" :key="res.id" class="item">
|
<div v-for="(res, idx) in resume.residencies" :key="idx" class="item">
|
||||||
<span class="item-title">{{ res.org }}</span>
|
<span class="item-title">{{ res.org }}</span>
|
||||||
<span class="item-meta">{{ res.date }}</span>
|
<span class="item-meta">{{ res.date }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="inline-flex">
|
<div class="inline-flex">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<IconButton :visible="work.score" type="score" :work="work" :link="work.score" class="inline-flex p-1"></IconButton>
|
<IconButton :visible="work.score" type="score" :work="work" :link="work.score" :newTab="true" class="inline-flex p-1"></IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IconButton :visible=item.entryTags.howpublished type="document" :link="item.entryTags.howpublished" class="inline-flex p-1"></IconButton>
|
<IconButton :visible=item.entryTags.howpublished type="document" :link="item.entryTags.howpublished" :newTab="true" class="inline-flex p-1"></IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,8 +66,8 @@
|
||||||
quality="50"/>
|
quality="50"/>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex place-content-center place-items-center">
|
<div class="flex place-content-center place-items-center">
|
||||||
<IconButton :visible="item.discogs_id" type="discogs" :link="'https://www.discogs.com/release/' + item.discogs_id"></IconButton>
|
<IconButton :visible="item.discogs_id" type="discogs" :link="'https://www.discogs.com/release/' + item.discogs_id" :newTab="true"></IconButton>
|
||||||
<IconButton :visible="item.buy_link" type="buy" :link="item.buy_link"></IconButton>
|
<IconButton :visible="item.buy_link" type="buy" :link="item.buy_link" :newTab="true"></IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
BIN
public/cv.pdf
Normal file
BIN
public/cv.pdf
Normal file
Binary file not shown.
1
public/debug.aux
Normal file
1
public/debug.aux
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
\relax
|
||||||
BIN
public/works_list.pdf
Normal file
BIN
public/works_list.pdf
Normal file
Binary file not shown.
242
scripts/generate-pdfs.js
Normal file
242
scripts/generate-pdfs.js
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
const DATA_DIR = path.join(__dirname, '../server/data')
|
||||||
|
const PUBLIC_DIR = path.join(__dirname, '../public')
|
||||||
|
|
||||||
|
function readJson(filename) {
|
||||||
|
const data = fs.readFileSync(path.join(DATA_DIR, filename), 'utf-8')
|
||||||
|
const parsed = JSON.parse(data)
|
||||||
|
|
||||||
|
function cleanDates(obj) {
|
||||||
|
if (Array.isArray(obj)) return obj.map(cleanDates)
|
||||||
|
if (obj && typeof obj === 'object') {
|
||||||
|
if (obj.$date?.$numberLong) return new Date(parseInt(obj.$date.$numberLong)).toISOString()
|
||||||
|
const cleaned = {}
|
||||||
|
for (const [k, v] of Object.entries(obj)) cleaned[k] = cleanDates(v)
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanDates(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMonth(dateStr) {
|
||||||
|
if (!dateStr) return 'Present'
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
if (isNaN(date)) return dateStr
|
||||||
|
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatYear(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
if (isNaN(date)) return dateStr
|
||||||
|
return date.getFullYear()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatShortDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
if (isNaN(date)) return dateStr
|
||||||
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeLatex(str) {
|
||||||
|
if (!str) return ''
|
||||||
|
str = str.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
return str.replace(/[&%$#_{}]/g, '\\$&').replace(/~/g, '\\textasciitilde{}').replace(/\^/g, '\\textasciicircum{}').replace(/ä/g, '{\\"a}').replace(/ö/g, '{\\"o}').replace(/ü/g, '{\\"u}').replace(/Ä/g, '{\\"A}').replace(/Ö/g, '{\\"O}').replace(/Ü/g, '{\\"U}').replace(/á/g, "\\'a").replace(/é/g, "\\'e").replace(/í/g, "\\'i").replace(/ó/g, "\\'o").replace(/ú/g, "\\'u").replace(/à/g, "\\`a").replace(/è/g, "\\`e").replace(/ì/g, "\\`i").replace(/ò/g, "\\`o").replace(/ù/g, "\\`u").replace(/ã/g, '{\\~a}').replace(/ñ/g, '{\\~n}').replace(/–/g, '--').replace(/—/g, '---').replace(/ç/g, '\\c c')
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCvLatex(resume, talks) {
|
||||||
|
const basics = resume.basics || {}
|
||||||
|
|
||||||
|
const talksByYear = {}
|
||||||
|
for (const talk of talks || []) {
|
||||||
|
if (!talk.date) continue
|
||||||
|
const year = new Date(talk.date).getFullYear()
|
||||||
|
if (!talksByYear[year]) talksByYear[year] = []
|
||||||
|
talksByYear[year].push(talk)
|
||||||
|
}
|
||||||
|
const sortedYears = Object.keys(talksByYear).sort((a, b) => b - a)
|
||||||
|
|
||||||
|
let latex = '\\documentclass[11pt]{article}\n'
|
||||||
|
latex += '\\usepackage[utf8]{inputenc}\n'
|
||||||
|
latex += '\\usepackage[T1]{fontenc}\n'
|
||||||
|
latex += '\\usepackage{geometry}\n'
|
||||||
|
latex += '\\geometry{letterpaper, top=1in, bottom=1in, left=1in, right=1in}\n'
|
||||||
|
latex += '\\usepackage{enumitem}\n'
|
||||||
|
latex += '\\usepackage{titlesec}\n'
|
||||||
|
latex += '\\pagestyle{empty}\n'
|
||||||
|
latex += '\\titleformat{\\section}{\\bfseries\\Large}{\\thesection}{1em}{}\n'
|
||||||
|
latex += '\\titleformat{\\subsection}{\\bfseries\\large}{\\thesubsection}{1em}{}\n'
|
||||||
|
latex += '\\titlespacing{\\section}{0pt}{12pt}{6pt}\n'
|
||||||
|
latex += '\\titlespacing{\\subsection}{0pt}{8pt}{4pt}\n'
|
||||||
|
latex += '\\begin{document}\n'
|
||||||
|
|
||||||
|
// Header
|
||||||
|
latex += '\\begin{center}\n'
|
||||||
|
latex += '{\\LARGE\\bfseries ' + escapeLatex(basics.name || '') + '}\\\\[4pt]\n'
|
||||||
|
latex += '{\\large Curriculum Vitae}\\\\[8pt]\n'
|
||||||
|
latex += escapeLatex(basics.email || '') + ' \\quad ' + escapeLatex(basics.phone || '') + ' \\quad ' + escapeLatex(basics.website || '') + '\n'
|
||||||
|
latex += '\\end{center}\n'
|
||||||
|
latex += '\\hrulefill\\\\[12pt]\n'
|
||||||
|
|
||||||
|
// Education
|
||||||
|
latex += '\\section{Education}\n'
|
||||||
|
for (const edu of resume.education || []) {
|
||||||
|
latex += '{\\bfseries ' + escapeLatex(edu.studyType) + ' in ' + escapeLatex(edu.area) + '}, ' + escapeLatex(edu.institution) + ', ' + formatYear(edu.endDate) + '\\\\[4pt]\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teaching
|
||||||
|
latex += '\\section{Teaching}\n'
|
||||||
|
for (const teach of resume.teaching || []) {
|
||||||
|
latex += '{\\bfseries ' + escapeLatex(teach.company) + '}, \\textit{' + escapeLatex(teach.position) + '}\\\\[2pt]\n'
|
||||||
|
latex += escapeLatex(formatMonth(teach.startDate)) + ' -- ' + escapeLatex(formatMonth(teach.endDate)) + '\\\\[8pt]\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lectures
|
||||||
|
latex += '\\section{Lectures}\n'
|
||||||
|
for (const year of sortedYears) {
|
||||||
|
latex += '\\subsection{' + year + '}\n'
|
||||||
|
for (const talk of talksByYear[year]) {
|
||||||
|
latex += '{\\bfseries ' + escapeLatex(talk.location) + '}\\\\\n'
|
||||||
|
const titles = Array.isArray(talk.title) ? talk.title : [talk.title]
|
||||||
|
for (const t of titles) latex += '\\hspace{8pt}\\textit{' + escapeLatex(t) + '}\\\\\n'
|
||||||
|
}
|
||||||
|
latex += '\\\\[4pt]\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relevant Work
|
||||||
|
latex += '\\section{Relevant Work}\n'
|
||||||
|
for (const w of resume.work || []) {
|
||||||
|
latex += '{\\bfseries ' + escapeLatex(w.company) + '}, \\textit{' + escapeLatex(w.position) + '}\\\\[2pt]\n'
|
||||||
|
latex += escapeLatex(formatMonth(w.startDate)) + ' -- ' + escapeLatex(formatMonth(w.endDate)) + '\\\\[8pt]\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skills
|
||||||
|
latex += '\\section{Skills}\n'
|
||||||
|
latex += (resume.skills?.[0]?.keywords?.map(s => escapeLatex(s)).join(', ') || '') + '\\\\[8pt]\n'
|
||||||
|
|
||||||
|
// Languages
|
||||||
|
latex += '\\section{Languages}\n'
|
||||||
|
latex += (resume.languages || []).map(l => escapeLatex(l.language) + ' (' + escapeLatex(l.fluency) + ')').join(', ') + '\\\\[8pt]\n'
|
||||||
|
|
||||||
|
// Recordings
|
||||||
|
latex += '\\section{Recordings}\n'
|
||||||
|
latex += '\\subsection{Solo Albums}\n'
|
||||||
|
for (const rel of resume.solo_releases || []) {
|
||||||
|
latex += '{\\bfseries ' + escapeLatex(rel.title) + '}. ' + escapeLatex(rel.publisher) + '. ' + escapeLatex(rel.media_type) + '. ' + escapeLatex(rel.date) + '.\\\\[4pt]\n'
|
||||||
|
}
|
||||||
|
latex += '\\subsection{Compilation Albums}\n'
|
||||||
|
for (const rel of resume.compilation_releases || []) {
|
||||||
|
latex += '{\\bfseries ' + escapeLatex(rel.title) + '}. ' + escapeLatex(rel.publisher) + '. ' + escapeLatex(rel.media_type) + '. ' + escapeLatex(rel.date) + '. \\textit{featuring ' + escapeLatex(rel.work) + '}.\\\\[4pt]\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Residencies
|
||||||
|
latex += '\\section{Residencies and Awards}\n'
|
||||||
|
for (const res of resume.residencies || []) {
|
||||||
|
latex += '{\\bfseries ' + escapeLatex(res.org) + '}, ' + escapeLatex(res.date) + '\\\\[4pt]\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// References
|
||||||
|
latex += '\\section{References}\n'
|
||||||
|
for (const ref of resume.references || []) {
|
||||||
|
latex += '{\\bfseries ' + escapeLatex(ref.name) + '}\\\\[2pt]\n'
|
||||||
|
latex += escapeLatex(ref.position) + '\\\\[2pt]\n'
|
||||||
|
latex += escapeLatex(ref.email) + '\\\\[8pt]\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
latex += '\\end{document}\n'
|
||||||
|
return latex
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWorksListLatex(resume, works, events) {
|
||||||
|
const basics = resume.basics || {}
|
||||||
|
|
||||||
|
const worksByYear = {}
|
||||||
|
for (const work of works || []) {
|
||||||
|
const year = work.date ? new Date(work.date).getFullYear() : 'Unknown'
|
||||||
|
if (!worksByYear[year]) worksByYear[year] = []
|
||||||
|
const workEvents = (events || []).filter(e => e.program && e.program.some(p => p.work?.toLowerCase().includes(work.title.toLowerCase())))
|
||||||
|
worksByYear[year].push({ ...work, events: workEvents })
|
||||||
|
}
|
||||||
|
const sortedYears = Object.keys(worksByYear).sort((a, b) => b - a)
|
||||||
|
|
||||||
|
let latex = '\\documentclass[11pt]{article}\n'
|
||||||
|
latex += '\\usepackage[utf8]{inputenc}\n'
|
||||||
|
latex += '\\usepackage[T1]{fontenc}\n'
|
||||||
|
latex += '\\usepackage{geometry}\n'
|
||||||
|
latex += '\\geometry{letterpaper, top=1in, bottom=1in, left=1in, right=1in}\n'
|
||||||
|
latex += '\\usepackage{enumitem}\n'
|
||||||
|
latex += '\\usepackage{titlesec}\n'
|
||||||
|
latex += '\\pagestyle{empty}\n'
|
||||||
|
latex += '\\titleformat{\\section}{\\bfseries\\Large}{\\thesection}{1em}{}\n'
|
||||||
|
latex += '\\titlespacing{\\section}{0pt}{12pt}{6pt}\n'
|
||||||
|
latex += '\\begin{document}\n'
|
||||||
|
|
||||||
|
// Header
|
||||||
|
latex += '\\begin{center}\n'
|
||||||
|
latex += '{\\LARGE\\bfseries ' + escapeLatex(basics.name || '') + '}\\\\[4pt]\n'
|
||||||
|
latex += '{\\large Works List with Presentation History}\\\\[8pt]\n'
|
||||||
|
latex += escapeLatex(basics.email || '') + ' \\quad ' + escapeLatex(basics.phone || '') + ' \\quad ' + escapeLatex(basics.website || '') + '\n'
|
||||||
|
latex += '\\end{center}\n'
|
||||||
|
latex += '\\hrulefill\\\\[12pt]\n'
|
||||||
|
|
||||||
|
latex += 'A chronological performance / exhibition history, scores, and recordings are available at\\\\\n'
|
||||||
|
latex += 'www.unboundedpress.org.\\\\\n'
|
||||||
|
latex += 'All scores are also published or forthcoming through Frog Peak at\\\\\n'
|
||||||
|
latex += 'www.frogpeak.org/fpartists/fpwinter.html.\\\\[12pt]\n'
|
||||||
|
latex += '\\hrulefill\\\\[12pt]\n'
|
||||||
|
|
||||||
|
for (const year of sortedYears) {
|
||||||
|
latex += '\\section{' + year + '}\n'
|
||||||
|
for (const work of worksByYear[year]) {
|
||||||
|
latex += '{\\bfseries\\textit{' + escapeLatex(work.title) + '}}\\\\[-4pt]\n'
|
||||||
|
if (work.instrument_tags) latex += '\\hspace{8pt}' + escapeLatex(work.instrument_tags.join(', ')) + '\\\\[-4pt]\n'
|
||||||
|
for (const event of work.events || []) {
|
||||||
|
const venue = event.venue || {}
|
||||||
|
latex += '\\hspace{8pt}' + escapeLatex(venue.name) + '; ' + escapeLatex(venue.city) + ', ' + escapeLatex(venue.state) + ' -- ' + escapeLatex(formatShortDate(event.start_date)) + '\\\\\n'
|
||||||
|
}
|
||||||
|
latex += '\\\\[4pt]\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latex += '\\end{document}\n'
|
||||||
|
return latex
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileLatex(latexContent, outputPath) {
|
||||||
|
const tempDir = path.join(__dirname, 'temp')
|
||||||
|
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true })
|
||||||
|
|
||||||
|
const texPath = path.join(tempDir, 'temp.tex')
|
||||||
|
fs.writeFileSync(texPath, latexContent)
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync('cd ' + tempDir + ' && pdflatex temp.tex', { stdio: 'pipe' })
|
||||||
|
const pdfPath = path.join(tempDir, 'temp.pdf')
|
||||||
|
if (fs.existsSync(pdfPath)) {
|
||||||
|
fs.copyFileSync(pdfPath, outputPath)
|
||||||
|
console.log('Generated:', outputPath)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
fs.writeFileSync(path.join(PUBLIC_DIR, 'debug.tex'), latexContent)
|
||||||
|
console.error('Error generating PDF')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resume = readJson('resume.json')[0]
|
||||||
|
const talks = readJson('talks.json')
|
||||||
|
const works = readJson('works.json')
|
||||||
|
const events = readJson('events.json')
|
||||||
|
|
||||||
|
console.log('Generating CV...')
|
||||||
|
compileLatex(generateCvLatex(resume, talks), path.join(PUBLIC_DIR, 'cv.pdf'))
|
||||||
|
|
||||||
|
console.log('Generating Works List...')
|
||||||
|
compileLatex(generateWorksListLatex(resume, works, events), path.join(PUBLIC_DIR, 'works_list.pdf'))
|
||||||
|
|
||||||
|
console.log('Done!')
|
||||||
|
|
@ -2,32 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/album_art.json'
|
const dataFile = './server/data/album_art.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const cleaned = { ...item }
|
|
||||||
|
|
||||||
if (cleaned._id?.$oid) {
|
|
||||||
cleaned.id = cleaned._id.$oid
|
|
||||||
delete cleaned._id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.chunkSize?.$numberLong) {
|
|
||||||
cleaned.chunkSize = parseInt(cleaned.chunkSize.$numberLong)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.uploadDate?.$date?.$numberLong) {
|
|
||||||
cleaned.uploadDate = new Date(parseInt(cleaned.uploadDate.$date.$numberLong)).toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.length?.$numberLong) {
|
|
||||||
cleaned.length = parseInt(cleaned.length.$numberLong)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/events.json'
|
const dataFile = './server/data/events.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const cleaned = { ...item }
|
|
||||||
if (cleaned._id?.$oid) {
|
|
||||||
cleaned.id = cleaned._id.$oid
|
|
||||||
delete cleaned._id
|
|
||||||
}
|
|
||||||
if (cleaned.start_date?.$date?.$numberLong) {
|
|
||||||
cleaned.start_date = new Date(parseInt(cleaned.start_date.$date.$numberLong)).toISOString()
|
|
||||||
}
|
|
||||||
if (cleaned.end_date?.$date?.$numberLong) {
|
|
||||||
cleaned.end_date = new Date(parseInt(cleaned.end_date.$date.$numberLong)).toISOString()
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/images.json'
|
const dataFile = './server/data/images.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const cleaned = { ...item }
|
|
||||||
|
|
||||||
if (cleaned._id?.$oid) {
|
|
||||||
cleaned.id = cleaned._id.$oid
|
|
||||||
delete cleaned._id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.chunkSize?.$numberLong) {
|
|
||||||
cleaned.chunkSize = parseInt(cleaned.chunkSize.$numberLong)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.uploadDate?.$date?.$numberLong) {
|
|
||||||
cleaned.uploadDate = new Date(parseInt(cleaned.uploadDate.$date.$numberLong)).toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.length?.$numberLong) {
|
|
||||||
cleaned.length = parseInt(cleaned.length.$numberLong)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/my_image_gallery.json'
|
const dataFile = './server/data/my_image_gallery.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const cleaned = { ...item }
|
|
||||||
if (cleaned._id?.$oid) {
|
|
||||||
cleaned.id = cleaned._id.$oid
|
|
||||||
delete cleaned._id
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/publications.json'
|
const dataFile = './server/data/publications.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const cleaned = { ...item }
|
|
||||||
if (cleaned._id?.$oid) {
|
|
||||||
cleaned.id = cleaned._id.$oid
|
|
||||||
delete cleaned._id
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/releases.json'
|
const dataFile = './server/data/releases.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const cleaned = { ...item }
|
|
||||||
if (cleaned._id?.$oid) {
|
|
||||||
cleaned.id = cleaned._id.$oid
|
|
||||||
delete cleaned._id
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/resume.json'
|
const dataFile = './server/data/resume.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return data.map(item => cleanItem(item))
|
|
||||||
}
|
|
||||||
return cleanItem(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanItem(item) {
|
|
||||||
if (item === null || item === undefined) return item
|
|
||||||
if (typeof item !== 'object') return item
|
|
||||||
|
|
||||||
const cleaned = {}
|
|
||||||
for (const [key, value] of Object.entries(item)) {
|
|
||||||
if (key === '_id' && value?.$oid) {
|
|
||||||
cleaned.id = value.$oid
|
|
||||||
} else if (typeof value === 'object') {
|
|
||||||
cleaned[key] = cleanData(value)
|
|
||||||
} else {
|
|
||||||
cleaned[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/talks.json'
|
const dataFile = './server/data/talks.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const cleaned = { ...item }
|
|
||||||
if (cleaned._id?.$oid) {
|
|
||||||
cleaned.id = cleaned._id.$oid
|
|
||||||
delete cleaned._id
|
|
||||||
}
|
|
||||||
if (cleaned.date?.$date?.$numberLong) {
|
|
||||||
cleaned.date = new Date(parseInt(cleaned.date.$date.$numberLong)).toISOString()
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
const dataFile = './server/data/works.json'
|
const dataFile = './server/data/works.json'
|
||||||
|
|
||||||
function cleanData(data) {
|
|
||||||
return data.map(item => {
|
|
||||||
const cleaned = { ...item }
|
|
||||||
if (cleaned._id?.$oid) {
|
|
||||||
cleaned.id = cleaned._id.$oid
|
|
||||||
delete cleaned._id
|
|
||||||
}
|
|
||||||
if (cleaned.date?.$date?.$numberLong) {
|
|
||||||
cleaned.date = new Date(parseInt(cleaned.date.$date.$numberLong)).toISOString()
|
|
||||||
}
|
|
||||||
if (cleaned.priority?.$numberInt) {
|
|
||||||
cleaned.priority = parseInt(cleaned.priority.$numberInt)
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
const raw = readFileSync(dataFile, 'utf-8')
|
const raw = readFileSync(dataFile, 'utf-8')
|
||||||
return cleanData(JSON.parse(raw))
|
return JSON.parse(raw)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
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
|
|
@ -1 +1,50 @@
|
||||||
[{"_id": {"$oid": "56f75398fdc52469b6c07c56"}, "image": "mbw_hundred_years_2.jpg", "credit": "Anton Lukoszevieze", "priority": {"$numberInt": "200"}}, {"_id": {"$oid": "5808be40ec6cea73b9674e76"}, "image": "mbw_oaxaca_1.jpg", "credit": "Bradford Bailey", "priority": {"$numberInt": "300"}}, {"_id": {"$oid": "5808be4fec6cea73b9674e77"}, "image": "mbw_oaxaca_2.jpg", "credit": "Bradford Bailey", "priority": {"$numberInt": "400"}}, {"_id": {"$oid": "5808be60ec6cea73b9674e78"}, "image": "mbw_oaxaca_3.jpg", "credit": "Bradford Bailey", "priority": {"$numberInt": "500"}}, {"_id": {"$oid": "5808ce03ec6cea73b9674e7a"}, "image": "mbw_hundred_years_1.jpg", "credit": "Anton Lukoszevieze", "priority": {"$numberInt": "100"}}, {"_id": {"$oid": "5808d232ec6cea73b9674e7c"}, "image": "mbw_ostrava_1.jpg", "credit": "ONMD", "priority": {"$numberInt": "600"}}, {"_id": {"$oid": "6016e58a2276b60007751e83"}, "image": "mbw_plants_foto.jpg", "credit": "Irasema Fern\u00e1ndez", "priority": {"$numberInt": "50"}}, {"_id": {"$oid": "6016e6cc2276b60007751e84"}, "image": "mbw_domino_perf.jpg", "credit": "Anna-Lena Reulein", "priority": {"$numberInt": "60"}}]
|
[
|
||||||
|
{
|
||||||
|
"id": "56f75398fdc52469b6c07c56",
|
||||||
|
"image": "mbw_hundred_years_2.jpg",
|
||||||
|
"credit": "Anton Lukoszevieze",
|
||||||
|
"priority": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5808be40ec6cea73b9674e76",
|
||||||
|
"image": "mbw_oaxaca_1.jpg",
|
||||||
|
"credit": "Bradford Bailey",
|
||||||
|
"priority": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5808be4fec6cea73b9674e77",
|
||||||
|
"image": "mbw_oaxaca_2.jpg",
|
||||||
|
"credit": "Bradford Bailey",
|
||||||
|
"priority": 400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5808be60ec6cea73b9674e78",
|
||||||
|
"image": "mbw_oaxaca_3.jpg",
|
||||||
|
"credit": "Bradford Bailey",
|
||||||
|
"priority": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5808ce03ec6cea73b9674e7a",
|
||||||
|
"image": "mbw_hundred_years_1.jpg",
|
||||||
|
"credit": "Anton Lukoszevieze",
|
||||||
|
"priority": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5808d232ec6cea73b9674e7c",
|
||||||
|
"image": "mbw_ostrava_1.jpg",
|
||||||
|
"credit": "ONMD",
|
||||||
|
"priority": 600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6016e58a2276b60007751e83",
|
||||||
|
"image": "mbw_plants_foto.jpg",
|
||||||
|
"credit": "Irasema Fernández",
|
||||||
|
"priority": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6016e6cc2276b60007751e84",
|
||||||
|
"image": "mbw_domino_perf.jpg",
|
||||||
|
"credit": "Anna-Lena Reulein",
|
||||||
|
"priority": 60
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1,219 @@
|
||||||
[{"_id": {"$oid": "563eb5a4dbb260acfadc4c35"}, "title": "west coast soundings", "label": "Edition Wandelweiser Records", "date": "2014", "discogs_id": "5764171", "buy_link": "https://www.wandelweiser.de/_e-w-records/_ewr-catalogue/ewr1404-05.html", "media_types": ["CD"], "album_art": "west_coast_soundings_cover.jpg", "pieces": [{"title": "small world", "performers": [{"name": "Frank Gratkowski", "instrument_tags": ["Alto Saxophone", "Bass Clarinet"]}, {"name": "Anton Lukoszevieze", "instrument_tags": ["Cello"]}, {"name": "Seth Josel", "instrument_tags": ["Electric Guitar"]}, {"name": "Hans W. Koch", "instrument_tags": ["Electronics"]}, {"name": "Lucia Mense", "instrument_tags": ["Tenor Recorder, Contrabass Recorder"]}]}]}, {"_id": {"$oid": "563eb670dbb260acfadc4c36"}, "title": "DIY Canons", "label": "Pogus Records", "date": "2005", "discogs_id": "859404", "buy_link": "http://www.pogus.com/21036.html", "media_types": ["CD"], "album_art": "DIY_Canons_cover.jpg", "pieces": [{"title": "Filter IV - P.I.X.L. Study No. 1"}]}, {"_id": {"$oid": "563eb762dbb260acfadc4c37"}, "title": "Music of Ostrava Days 2009 Live", "label": "Ostrava Center for New Music", "date": "2009", "discogs_id": "2552681", "buy_link": "https://www.newmusicostrava.cz/en/articles/1541-2cd-music-of-ostrava-days-2009-live.html", "media_types": ["CD"], "album_art": "Ostrava_cover.jpg", "pieces": [{"title": "recitation, code, and (perhaps) round", "ensemble": "Canticum Ostrava"}]}, {"_id": {"$oid": "563eb819dbb260acfadc4c38"}, "title": "rounds", "label": "the wulf. records", "date": "2013", "buy_link": "https://awavepress.bandcamp.com/album/rounds", "media_types": ["Vinyl"], "album_art": "Rounds_cover.jpg", "pieces": [{"title": "welcome round for larry"}]}, {"_id": {"$oid": "5a4940b705a681a2659a0efe"}, "title": "approximating omega", "label": "Edition Wandelweiser Records", "date": "2017", "discogs_id": "11511306", "buy_link": "https://www.wandelweiser.de/_e-w-records/_ewr-catalogue/ewr1718.html", "media_types": ["CD"], "album_art": "approximating_omega_cover.jpg", "pieces": [{"title": "Approximating Omega"}, {"title": "for gregory chaitin"}]}, {"_id": {"$oid": "5a49621905a681a2659a0eff"}, "title": "lower limit", "label": "New World Records", "date": "2018", "discogs_id": "11511594", "buy_link": "https://www.newworldrecords.org/products/michael-winter-lower-limit", "media_types": ["CD"], "album_art": "lower_limit_cover.jpg", "pieces": [{"title": "necklaces"}, {"title": "mass and band"}, {"title": "chorale and finely tuned resonators"}, {"title": "lower limit"}]}, {"_id": {"$oid": "5b46022cd66afc4a413f148c"}, "title": "preliminary thoughts", "label": "Bahn Mi Verlag", "date": "2018", "discogs_id": "12273309", "buy_link": "https://banhmiverlag.bandcamp.com/album/preliminary-thoughts", "media_types": ["Cassette Tape"], "album_art": "preliminary_thoughts_BMV.jpg", "pieces": [{"title": "preliminary thoughts"}]}, {"_id": {"$oid": "5b460243d66afc4a413f1495"}, "title": "preliminary thoughts", "label": "Tsonami Records", "date": "2018", "discogs_id": "12335728", "buy_link": "https://www.tsonami.cl/tienda/michael-winter/", "media_types": ["CD"], "album_art": "preliminary_thoughts_TR.jpg", "pieces": [{"title": "preliminary thoughts"}]}, {"_id": {"$oid": "6016c9742276b60007751e82"}, "title": "single track", "label": "Another Timbre", "date": "2021", "discogs_id": "17283877", "buy_link": "http://www.anothertimbre.com/michaelwinter.html", "media_types": ["CD"], "album_art": "single_track_cover.jpg", "pieces": [{"title": "single track"}]}, {"_id": {"$oid": "61f777b0dc86b900073f369d"}, "title": "Counterfeiting in Colonial Connecticut", "label": "XI Records", "date": "2022", "discogs_id": "", "buy_link": "https://michaelwinter.bandcamp.com/album/counterfeiting-in-colonial-connecticut-a-lot-of-tiles-trivial-scan", "media_types": ["CD"], "album_art": "CiCC_cover.jpg", "pieces": [{"title": "Counterfeiting in Colonial Connecticut"}, {"title": "a lot of tiles (trivial scan)"}]}]
|
[
|
||||||
|
{
|
||||||
|
"id": "563eb5a4dbb260acfadc4c35",
|
||||||
|
"title": "west coast soundings",
|
||||||
|
"label": "Edition Wandelweiser Records",
|
||||||
|
"date": "2014",
|
||||||
|
"discogs_id": "5764171",
|
||||||
|
"buy_link": "https://www.wandelweiser.de/_e-w-records/_ewr-catalogue/ewr1404-05.html",
|
||||||
|
"media_types": [
|
||||||
|
"CD"
|
||||||
|
],
|
||||||
|
"album_art": "west_coast_soundings_cover.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "small world",
|
||||||
|
"performers": [
|
||||||
|
{
|
||||||
|
"name": "Frank Gratkowski",
|
||||||
|
"instrument_tags": [
|
||||||
|
"Alto Saxophone",
|
||||||
|
"Bass Clarinet"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Anton Lukoszevieze",
|
||||||
|
"instrument_tags": [
|
||||||
|
"Cello"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Seth Josel",
|
||||||
|
"instrument_tags": [
|
||||||
|
"Electric Guitar"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hans W. Koch",
|
||||||
|
"instrument_tags": [
|
||||||
|
"Electronics"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lucia Mense",
|
||||||
|
"instrument_tags": [
|
||||||
|
"Tenor Recorder, Contrabass Recorder"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "563eb670dbb260acfadc4c36",
|
||||||
|
"title": "DIY Canons",
|
||||||
|
"label": "Pogus Records",
|
||||||
|
"date": "2005",
|
||||||
|
"discogs_id": "859404",
|
||||||
|
"buy_link": "http://www.pogus.com/21036.html",
|
||||||
|
"media_types": [
|
||||||
|
"CD"
|
||||||
|
],
|
||||||
|
"album_art": "DIY_Canons_cover.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "Filter IV - P.I.X.L. Study No. 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "563eb762dbb260acfadc4c37",
|
||||||
|
"title": "Music of Ostrava Days 2009 Live",
|
||||||
|
"label": "Ostrava Center for New Music",
|
||||||
|
"date": "2009",
|
||||||
|
"discogs_id": "2552681",
|
||||||
|
"buy_link": "https://www.newmusicostrava.cz/en/articles/1541-2cd-music-of-ostrava-days-2009-live.html",
|
||||||
|
"media_types": [
|
||||||
|
"CD"
|
||||||
|
],
|
||||||
|
"album_art": "Ostrava_cover.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "recitation, code, and (perhaps) round",
|
||||||
|
"ensemble": "Canticum Ostrava"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "563eb819dbb260acfadc4c38",
|
||||||
|
"title": "rounds",
|
||||||
|
"label": "the wulf. records",
|
||||||
|
"date": "2013",
|
||||||
|
"buy_link": "https://awavepress.bandcamp.com/album/rounds",
|
||||||
|
"media_types": [
|
||||||
|
"Vinyl"
|
||||||
|
],
|
||||||
|
"album_art": "Rounds_cover.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "welcome round for larry"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5a4940b705a681a2659a0efe",
|
||||||
|
"title": "approximating omega",
|
||||||
|
"label": "Edition Wandelweiser Records",
|
||||||
|
"date": "2017",
|
||||||
|
"discogs_id": "11511306",
|
||||||
|
"buy_link": "https://www.wandelweiser.de/_e-w-records/_ewr-catalogue/ewr1718.html",
|
||||||
|
"media_types": [
|
||||||
|
"CD"
|
||||||
|
],
|
||||||
|
"album_art": "approximating_omega_cover.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "Approximating Omega"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "for gregory chaitin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5a49621905a681a2659a0eff",
|
||||||
|
"title": "lower limit",
|
||||||
|
"label": "New World Records",
|
||||||
|
"date": "2018",
|
||||||
|
"discogs_id": "11511594",
|
||||||
|
"buy_link": "https://www.newworldrecords.org/products/michael-winter-lower-limit",
|
||||||
|
"media_types": [
|
||||||
|
"CD"
|
||||||
|
],
|
||||||
|
"album_art": "lower_limit_cover.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "necklaces"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "mass and band"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "chorale and finely tuned resonators"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "lower limit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5b46022cd66afc4a413f148c",
|
||||||
|
"title": "preliminary thoughts",
|
||||||
|
"label": "Bahn Mi Verlag",
|
||||||
|
"date": "2018",
|
||||||
|
"discogs_id": "12273309",
|
||||||
|
"buy_link": "https://banhmiverlag.bandcamp.com/album/preliminary-thoughts",
|
||||||
|
"media_types": [
|
||||||
|
"Cassette Tape"
|
||||||
|
],
|
||||||
|
"album_art": "preliminary_thoughts_BMV.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "preliminary thoughts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5b460243d66afc4a413f1495",
|
||||||
|
"title": "preliminary thoughts",
|
||||||
|
"label": "Tsonami Records",
|
||||||
|
"date": "2018",
|
||||||
|
"discogs_id": "12335728",
|
||||||
|
"buy_link": "https://www.tsonami.cl/tienda/michael-winter/",
|
||||||
|
"media_types": [
|
||||||
|
"CD"
|
||||||
|
],
|
||||||
|
"album_art": "preliminary_thoughts_TR.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "preliminary thoughts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6016c9742276b60007751e82",
|
||||||
|
"title": "single track",
|
||||||
|
"label": "Another Timbre",
|
||||||
|
"date": "2021",
|
||||||
|
"discogs_id": "17283877",
|
||||||
|
"buy_link": "http://www.anothertimbre.com/michaelwinter.html",
|
||||||
|
"media_types": [
|
||||||
|
"CD"
|
||||||
|
],
|
||||||
|
"album_art": "single_track_cover.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "single track"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "61f777b0dc86b900073f369d",
|
||||||
|
"title": "Counterfeiting in Colonial Connecticut",
|
||||||
|
"label": "XI Records",
|
||||||
|
"date": "2022",
|
||||||
|
"buy_link": "https://michaelwinter.bandcamp.com/album/counterfeiting-in-colonial-connecticut-a-lot-of-tiles-trivial-scan",
|
||||||
|
"media_types": [
|
||||||
|
"CD"
|
||||||
|
],
|
||||||
|
"album_art": "CiCC_cover.jpg",
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"title": "Counterfeiting in Colonial Connecticut"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "a lot of tiles (trivial scan)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
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
Loading…
Reference in a new issue